Files
AetherForge/server/internal/db/clearance.go
AetherForge f89ba94cb7 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.
2026-06-07 02:30:00 -07:00

101 lines
2.7 KiB
Go

package db
import (
"database/sql"
"fmt"
"strings"
)
// ClearanceEvent is one clearance elevation audit row.
type ClearanceEvent struct {
ID int64 `json:"id"`
AgentID string `json:"agent_id"`
FromLevel int `json:"from_level"`
ToLevel int `json:"to_level"`
Reason string `json:"reason"`
Source string `json:"source"`
Timestamp string `json:"ts"`
}
func (d *Database) ensureClearanceEventsTable() error {
_, err := d.Exec(`CREATE TABLE IF NOT EXISTS clearance_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL,
from_level INTEGER NOT NULL DEFAULT 0,
to_level INTEGER NOT NULL DEFAULT 0,
reason TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT '',
ts DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
return err
}
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_clearance_events_agent ON clearance_events(agent_id)`)
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_clearance_events_ts ON clearance_events(ts)`)
return nil
}
// InsertClearanceEvent logs a clearance elevation.
func (d *Database) InsertClearanceEvent(agentID string, fromLevel, toLevel int, reason, source string) error {
if d == nil {
return nil
}
if err := d.ensureClearanceEventsTable(); err != nil {
return err
}
_, err := d.Exec(
`INSERT INTO clearance_events (agent_id, from_level, to_level, reason, source) VALUES (?, ?, ?, ?, ?)`,
agentID, fromLevel, toLevel, reason, source,
)
return err
}
// ListClearanceEvents returns recent clearance events for an agent (or fleet-wide when agentID empty).
func (d *Database) ListClearanceEvents(agentID string, limit int) ([]ClearanceEvent, error) {
if d == nil {
return nil, nil
}
if err := d.ensureClearanceEventsTable(); err != nil {
return nil, err
}
if limit <= 0 {
limit = 50
}
if limit > 500 {
limit = 500
}
var rows *sql.Rows
var err error
agentID = strings.TrimSpace(agentID)
if agentID != "" {
rows, err = d.Query(
`SELECT id, agent_id, from_level, to_level, reason, source, ts
FROM clearance_events WHERE agent_id = ? ORDER BY id DESC LIMIT ?`,
agentID, limit,
)
} else {
rows, err = d.Query(
`SELECT id, agent_id, from_level, to_level, reason, source, ts
FROM clearance_events ORDER BY id DESC LIMIT ?`,
limit,
)
}
if err != nil {
return nil, fmt.Errorf("list clearance events: %w", err)
}
defer rows.Close()
out := make([]ClearanceEvent, 0, limit)
for rows.Next() {
var rec ClearanceEvent
var ts string
if err := rows.Scan(&rec.ID, &rec.AgentID, &rec.FromLevel, &rec.ToLevel, &rec.Reason, &rec.Source, &ts); err != nil {
return nil, err
}
rec.Timestamp = ts
out = append(out, rec)
}
return out, rows.Err()
}