gomcp/internal/domain/soc/zerog.go

184 lines
4.4 KiB
Go

package soc
import (
"fmt"
"sync"
"time"
)
// ZeroGMode implements §13.4 — manual approval workflow for Strike Force operations.
// Events in Zero-G mode require explicit analyst approval before auto-response executes.
type ZeroGMode struct {
mu sync.RWMutex
enabled bool
queue []ZeroGRequest
resolved []ZeroGRequest
maxQueue int
}
// ZeroGRequest represents a pending approval request.
type ZeroGRequest struct {
ID string `json:"id"`
EventID string `json:"event_id"`
IncidentID string `json:"incident_id,omitempty"`
Action string `json:"action"` // What would auto-execute
Severity string `json:"severity"`
Description string `json:"description"`
Status ZeroGStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
ResolvedBy string `json:"resolved_by,omitempty"`
Verdict ZeroGVerdict `json:"verdict,omitempty"`
}
// ZeroGStatus tracks the request lifecycle.
type ZeroGStatus string
const (
ZeroGPending ZeroGStatus = "PENDING"
ZeroGApproved ZeroGStatus = "APPROVED"
ZeroGDenied ZeroGStatus = "DENIED"
ZeroGExpired ZeroGStatus = "EXPIRED"
)
// ZeroGVerdict is the analyst's decision.
type ZeroGVerdict string
const (
ZGVerdictApprove ZeroGVerdict = "APPROVE"
ZGVerdictDeny ZeroGVerdict = "DENY"
ZGVerdictEscalate ZeroGVerdict = "ESCALATE"
)
// NewZeroGMode creates the Zero-G approval engine.
func NewZeroGMode() *ZeroGMode {
return &ZeroGMode{
enabled: false,
maxQueue: 200,
}
}
// Enable activates Zero-G mode (manual approval required).
func (z *ZeroGMode) Enable() {
z.mu.Lock()
defer z.mu.Unlock()
z.enabled = true
}
// Disable deactivates Zero-G mode (auto-response resumes).
func (z *ZeroGMode) Disable() {
z.mu.Lock()
defer z.mu.Unlock()
z.enabled = false
}
// IsEnabled returns whether Zero-G mode is active.
func (z *ZeroGMode) IsEnabled() bool {
z.mu.RLock()
defer z.mu.RUnlock()
return z.enabled
}
// RequestApproval queues an action for manual approval. Returns the request ID.
func (z *ZeroGMode) RequestApproval(eventID, incidentID, action, severity, description string) string {
z.mu.Lock()
defer z.mu.Unlock()
if !z.enabled {
return "" // Not in Zero-G mode, skip
}
reqID := fmt.Sprintf("zg-%d", time.Now().UnixNano())
req := ZeroGRequest{
ID: reqID,
EventID: eventID,
IncidentID: incidentID,
Action: action,
Severity: severity,
Description: description,
Status: ZeroGPending,
CreatedAt: time.Now(),
}
// Enforce max queue size
if len(z.queue) >= z.maxQueue {
// Expire oldest
expired := z.queue[0]
expired.Status = ZeroGExpired
now := time.Now()
expired.ResolvedAt = &now
z.resolved = append(z.resolved, expired)
z.queue = z.queue[1:]
}
z.queue = append(z.queue, req)
return reqID
}
// Resolve processes an analyst's verdict on a pending request.
func (z *ZeroGMode) Resolve(requestID string, verdict ZeroGVerdict, analyst string) error {
z.mu.Lock()
defer z.mu.Unlock()
for i, req := range z.queue {
if req.ID == requestID {
now := time.Now()
z.queue[i].ResolvedAt = &now
z.queue[i].ResolvedBy = analyst
z.queue[i].Verdict = verdict
switch verdict {
case ZGVerdictApprove:
z.queue[i].Status = ZeroGApproved
case ZGVerdictDeny:
z.queue[i].Status = ZeroGDenied
case ZGVerdictEscalate:
z.queue[i].Status = ZeroGPending // Stay pending, but mark escalated
}
// Move to resolved
z.resolved = append(z.resolved, z.queue[i])
z.queue = append(z.queue[:i], z.queue[i+1:]...)
return nil
}
}
return fmt.Errorf("zero-g request %s not found", requestID)
}
// PendingRequests returns all pending approval requests.
func (z *ZeroGMode) PendingRequests() []ZeroGRequest {
z.mu.RLock()
defer z.mu.RUnlock()
result := make([]ZeroGRequest, len(z.queue))
copy(result, z.queue)
return result
}
// Stats returns Zero-G mode statistics.
func (z *ZeroGMode) Stats() map[string]any {
z.mu.RLock()
defer z.mu.RUnlock()
approved := 0
denied := 0
expired := 0
for _, r := range z.resolved {
switch r.Status {
case ZeroGApproved:
approved++
case ZeroGDenied:
denied++
case ZeroGExpired:
expired++
}
}
return map[string]any{
"enabled": z.enabled,
"pending": len(z.queue),
"total_resolved": len(z.resolved),
"approved": approved,
"denied": denied,
"expired": expired,
}
}