Add L0-L4 security clearance for fleet commands and AI elevation.
Gate manual and AI commands by per-agent clearance, auto-elevate stuck hosts to L4 when AI mode allows, and surface clearance in Access Depth and LOTL timeline.
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/clearance"
|
||||
)
|
||||
|
||||
type mockSnap struct {
|
||||
@@ -38,7 +40,7 @@ type mockStore struct {
|
||||
rows []string
|
||||
}
|
||||
|
||||
func (m *mockStore) InsertAIDecision(_, _, _, executed string) error {
|
||||
func (m *mockStore) InsertAIDecision(_, _, _, executed string, _ *CourtDecisionMeta) error {
|
||||
m.mu.Lock()
|
||||
m.rows = append(m.rows, executed)
|
||||
m.mu.Unlock()
|
||||
@@ -59,6 +61,8 @@ func TestSchedulerExecutesRestartCommand(t *testing.T) {
|
||||
&mockSnap{ids: []string{"agent-1"}, snap: AgentSnapshot{AgentID: "agent-1", Name: "host"}},
|
||||
exec,
|
||||
store,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
sched.lastRun["agent-1"] = time.Now().Add(-2 * time.Minute)
|
||||
sched.Tick()
|
||||
@@ -84,9 +88,77 @@ func TestSchedulerNoOpWhenDisabled(t *testing.T) {
|
||||
&mockSnap{ids: []string{"agent-1"}},
|
||||
exec,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
sched.Tick()
|
||||
if len(exec.calls) != 0 {
|
||||
t.Fatalf("expected no calls")
|
||||
}
|
||||
}
|
||||
|
||||
type mockElevator struct {
|
||||
mu sync.Mutex
|
||||
requests []struct {
|
||||
agentID string
|
||||
toLevel int
|
||||
reason string
|
||||
source string
|
||||
}
|
||||
level int
|
||||
}
|
||||
|
||||
func (m *mockElevator) Level(string) int {
|
||||
if m.level > 0 {
|
||||
return m.level
|
||||
}
|
||||
return clearance.L1
|
||||
}
|
||||
|
||||
func (m *mockElevator) RequestElevation(agentID string, toLevel int, reason, source string) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.requests = append(m.requests, struct {
|
||||
agentID string
|
||||
toLevel int
|
||||
reason string
|
||||
source string
|
||||
}{agentID, toLevel, reason, source})
|
||||
m.level = toLevel
|
||||
return toLevel, nil
|
||||
}
|
||||
|
||||
func TestSchedulerStuckHostTriggersL4Elevation(t *testing.T) {
|
||||
old := DecideFunc
|
||||
defer func() { DecideFunc = old }()
|
||||
DecideFunc = func(_ context.Context, _, _, _, _ string) (string, error) {
|
||||
return `{"commands":[{"type":"noop","args":{}}]}`, nil
|
||||
}
|
||||
|
||||
elevator := &mockElevator{}
|
||||
sched := NewScheduler(
|
||||
&mockCfg{cfg: Config{Enabled: true, Endpoint: "http://test/v1", IntervalSec: 1, AutoElevateClearance: true}},
|
||||
&mockSnap{
|
||||
ids: []string{"stuck-1"},
|
||||
snap: AgentSnapshot{
|
||||
AgentID: "stuck-1", Name: "host", Stuck: true, FailedTierCount: 14,
|
||||
},
|
||||
},
|
||||
&mockExec{},
|
||||
&mockStore{},
|
||||
nil,
|
||||
elevator,
|
||||
)
|
||||
sched.lastRun["stuck-1"] = time.Now().Add(-2 * time.Minute)
|
||||
sched.Tick()
|
||||
|
||||
elevator.mu.Lock()
|
||||
defer elevator.mu.Unlock()
|
||||
if len(elevator.requests) != 1 {
|
||||
t.Fatalf("expected 1 elevation, got %+v", elevator.requests)
|
||||
}
|
||||
req := elevator.requests[0]
|
||||
if req.toLevel != clearance.L4 || req.reason != "stuck host recovery" {
|
||||
t.Fatalf("unexpected elevation: %+v", req)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user