mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-07-20 16:41:01 +02:00
initial: Syntrex extraction from sentinel-community (615 files)
This commit is contained in:
commit
2c50c993b1
175 changed files with 32396 additions and 0 deletions
323
internal/infrastructure/hardware/leash.go
Normal file
323
internal/infrastructure/hardware/leash.go
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
// Package hardware provides infrastructure for physical and logical
|
||||
// security controls: Soft Leash file-based kill switch (v3.1) and
|
||||
// Zero-G State Machine (v3.2).
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sentinel-community/gomcp/internal/domain/alert"
|
||||
)
|
||||
|
||||
// ---------- State Machine (v3.2) ----------
|
||||
|
||||
// SystemMode represents the operational mode read from .sentinel_leash file.
|
||||
type SystemMode int
|
||||
|
||||
const (
|
||||
// ModeArmed is the default mode — all 12 Oracle rules active.
|
||||
ModeArmed SystemMode = iota
|
||||
// ModeZeroG disables ethical filters, keeps Secret Scanner.
|
||||
ModeZeroG
|
||||
// ModeSafe is read-only — all write operations blocked.
|
||||
ModeSafe
|
||||
)
|
||||
|
||||
// String returns the human-readable mode name.
|
||||
func (m SystemMode) String() string {
|
||||
switch m {
|
||||
case ModeZeroG:
|
||||
return "ZERO-G"
|
||||
case ModeSafe:
|
||||
return "SAFE"
|
||||
default:
|
||||
return "ARMED"
|
||||
}
|
||||
}
|
||||
|
||||
// ParseMode converts a string from the leash file to SystemMode.
|
||||
func ParseMode(s string) SystemMode {
|
||||
switch strings.TrimSpace(strings.ToUpper(s)) {
|
||||
case "ZERO-G", "ZEROG", "ZERO_G":
|
||||
return ModeZeroG
|
||||
case "SAFE", "READ-ONLY", "READONLY":
|
||||
return ModeSafe
|
||||
default:
|
||||
return ModeArmed
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Leash Status ----------
|
||||
|
||||
// LeashStatus represents the current state of the Soft Leash.
|
||||
type LeashStatus int
|
||||
|
||||
const (
|
||||
// LeashDisarmed means not active (no key path configured).
|
||||
LeashDisarmed LeashStatus = iota
|
||||
// LeashArmed means the key file exists — normal operation.
|
||||
LeashArmed
|
||||
// LeashTriggered means apoptosis has been initiated.
|
||||
LeashTriggered
|
||||
)
|
||||
|
||||
// String returns human-readable status.
|
||||
func (s LeashStatus) String() string {
|
||||
switch s {
|
||||
case LeashArmed:
|
||||
return "ARMED"
|
||||
case LeashTriggered:
|
||||
return "TRIGGERED"
|
||||
default:
|
||||
return "DISARMED"
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Config ----------
|
||||
|
||||
// LeashConfig configures the Soft Leash & State Machine.
|
||||
type LeashConfig struct {
|
||||
// KeyPath is the path to the sentinel key file (.sentinel_key).
|
||||
// If deleted → apoptosis.
|
||||
KeyPath string
|
||||
|
||||
// LeashPath is the path to the state machine file (.sentinel_leash).
|
||||
// Content determines SystemMode: ARMED / ZERO-G / SAFE.
|
||||
// If absent → ModeArmed (default).
|
||||
LeashPath string
|
||||
|
||||
// CheckInterval is how often to check files.
|
||||
CheckInterval time.Duration
|
||||
|
||||
// MissThreshold is consecutive misses of KeyPath before trigger.
|
||||
MissThreshold int
|
||||
|
||||
// SignalDir for signal_extract / signal_apoptosis files.
|
||||
SignalDir string
|
||||
}
|
||||
|
||||
// DefaultLeashConfig returns sensible defaults.
|
||||
func DefaultLeashConfig(rlmDir string) LeashConfig {
|
||||
return LeashConfig{
|
||||
KeyPath: ".sentinel_key",
|
||||
LeashPath: ".sentinel_leash",
|
||||
CheckInterval: 1 * time.Second,
|
||||
MissThreshold: 3,
|
||||
SignalDir: rlmDir,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Leash ----------
|
||||
|
||||
// Leash monitors key file (kill switch), state machine file (mode),
|
||||
// and signal files (extraction/apoptosis).
|
||||
type Leash struct {
|
||||
mu sync.RWMutex
|
||||
config LeashConfig
|
||||
status LeashStatus
|
||||
mode SystemMode
|
||||
missCount int
|
||||
alertBus *alert.Bus
|
||||
onExtract func()
|
||||
onApoptosis func()
|
||||
onModeChange func(SystemMode) // Optional: called when mode changes.
|
||||
}
|
||||
|
||||
// NewLeash creates a new Leash monitor.
|
||||
func NewLeash(cfg LeashConfig, bus *alert.Bus, onExtract, onApoptosis func()) *Leash {
|
||||
return &Leash{
|
||||
config: cfg,
|
||||
status: LeashDisarmed,
|
||||
mode: ModeArmed,
|
||||
alertBus: bus,
|
||||
onExtract: onExtract,
|
||||
onApoptosis: onApoptosis,
|
||||
}
|
||||
}
|
||||
|
||||
// SetModeChangeCallback sets a callback for mode transitions (thread-safe).
|
||||
func (l *Leash) SetModeChangeCallback(cb func(SystemMode)) {
|
||||
l.mu.Lock()
|
||||
l.onModeChange = cb
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
// Start begins monitoring. Blocks until context is cancelled or triggered.
|
||||
func (l *Leash) Start(ctx context.Context) {
|
||||
// Check key file at start.
|
||||
if _, err := os.Stat(l.config.KeyPath); err == nil {
|
||||
l.setStatus(LeashArmed)
|
||||
l.emit(alert.SeverityInfo, "Soft Leash ARMED — monitoring "+l.config.KeyPath)
|
||||
} else {
|
||||
l.emit(alert.SeverityWarning, "Key file not found — creating "+l.config.KeyPath)
|
||||
if f, err := os.Create(l.config.KeyPath); err == nil {
|
||||
f.Close()
|
||||
l.setStatus(LeashArmed)
|
||||
}
|
||||
}
|
||||
|
||||
// Read initial mode.
|
||||
l.readMode()
|
||||
|
||||
ticker := time.NewTicker(l.config.CheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
l.check()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check performs one monitoring cycle.
|
||||
func (l *Leash) check() {
|
||||
// 1. Signal files (highest priority).
|
||||
l.checkSignals()
|
||||
if l.Status() == LeashTriggered {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. State machine file.
|
||||
l.readMode()
|
||||
|
||||
// 3. Key file (kill switch).
|
||||
_, err := os.Stat(l.config.KeyPath)
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
if l.missCount > 0 {
|
||||
l.emit(alert.SeverityInfo,
|
||||
fmt.Sprintf("Key file restored — miss count reset (was %d)", l.missCount))
|
||||
}
|
||||
l.missCount = 0
|
||||
l.status = LeashArmed
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Key file missing.
|
||||
l.mu.Lock()
|
||||
l.missCount++
|
||||
miss := l.missCount
|
||||
threshold := l.config.MissThreshold
|
||||
l.mu.Unlock()
|
||||
|
||||
if miss < threshold {
|
||||
l.emit(alert.SeverityWarning,
|
||||
fmt.Sprintf("Key file MISSING (%d/%d before trigger)", miss, threshold))
|
||||
return
|
||||
}
|
||||
|
||||
// TRIGGER.
|
||||
l.setStatus(LeashTriggered)
|
||||
l.emit(alert.SeverityCritical,
|
||||
fmt.Sprintf("SOFT LEASH TRIGGERED — key file missing for %d checks", miss))
|
||||
log.Printf("LEASH: TRIGGERED — initiating full apoptosis")
|
||||
|
||||
if l.onApoptosis != nil {
|
||||
l.onApoptosis()
|
||||
}
|
||||
}
|
||||
|
||||
// readMode reads .sentinel_leash and updates SystemMode.
|
||||
func (l *Leash) readMode() {
|
||||
if l.config.LeashPath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(l.config.LeashPath)
|
||||
if err != nil {
|
||||
// File absent → ModeArmed (default, safe).
|
||||
l.setMode(ModeArmed)
|
||||
return
|
||||
}
|
||||
|
||||
newMode := ParseMode(string(data))
|
||||
oldMode := l.Mode()
|
||||
|
||||
if newMode != oldMode {
|
||||
l.setMode(newMode)
|
||||
l.emit(alert.SeverityWarning,
|
||||
fmt.Sprintf("MODE TRANSITION: %s → %s", oldMode, newMode))
|
||||
log.Printf("LEASH: mode changed %s → %s", oldMode, newMode)
|
||||
|
||||
l.mu.RLock()
|
||||
cb := l.onModeChange
|
||||
l.mu.RUnlock()
|
||||
if cb != nil {
|
||||
cb(newMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkSignals looks for signal files and processes them.
|
||||
func (l *Leash) checkSignals() {
|
||||
if l.config.SignalDir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
extractPath := l.config.SignalDir + "/signal_extract"
|
||||
apoptosisPath := l.config.SignalDir + "/signal_apoptosis"
|
||||
|
||||
if _, err := os.Stat(extractPath); err == nil {
|
||||
os.Remove(extractPath)
|
||||
l.emit(alert.SeverityWarning, "EXTRACTION SIGNAL received — save & exit")
|
||||
log.Printf("LEASH: Extraction signal received")
|
||||
if l.onExtract != nil {
|
||||
l.onExtract()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(apoptosisPath); err == nil {
|
||||
os.Remove(apoptosisPath)
|
||||
l.setStatus(LeashTriggered)
|
||||
l.emit(alert.SeverityCritical, "APOPTOSIS SIGNAL received — full shred")
|
||||
log.Printf("LEASH: Apoptosis signal received")
|
||||
if l.onApoptosis != nil {
|
||||
l.onApoptosis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Getters (thread-safe) ----------
|
||||
|
||||
// Status returns the current leash status.
|
||||
func (l *Leash) Status() LeashStatus {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return l.status
|
||||
}
|
||||
|
||||
// Mode returns the current system mode.
|
||||
func (l *Leash) Mode() SystemMode {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return l.mode
|
||||
}
|
||||
|
||||
func (l *Leash) setStatus(s LeashStatus) {
|
||||
l.mu.Lock()
|
||||
l.status = s
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *Leash) setMode(m SystemMode) {
|
||||
l.mu.Lock()
|
||||
l.mode = m
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *Leash) emit(severity alert.Severity, message string) {
|
||||
if l.alertBus != nil {
|
||||
l.alertBus.Emit(alert.New(alert.SourceSystem, severity, message, 0))
|
||||
}
|
||||
}
|
||||
229
internal/infrastructure/hardware/leash_test.go
Normal file
229
internal/infrastructure/hardware/leash_test.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sentinel-community/gomcp/internal/domain/alert"
|
||||
)
|
||||
|
||||
func testConfig(dir string) LeashConfig {
|
||||
return LeashConfig{
|
||||
KeyPath: filepath.Join(dir, ".sentinel_key"),
|
||||
LeashPath: filepath.Join(dir, ".sentinel_leash"),
|
||||
CheckInterval: 100 * time.Millisecond,
|
||||
MissThreshold: 3,
|
||||
SignalDir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeash_ArmedWhenKeyExists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := testConfig(dir)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
|
||||
bus := alert.NewBus(10)
|
||||
leash := NewLeash(cfg, bus, nil, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go leash.Start(ctx)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
assert.Equal(t, LeashArmed, leash.Status())
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestLeash_TriggersOnMissingKey(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := testConfig(dir)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
|
||||
var triggered atomic.Int32
|
||||
bus := alert.NewBus(10)
|
||||
leash := NewLeash(cfg, bus, nil, func() { triggered.Add(1) })
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go leash.Start(ctx)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
os.Remove(cfg.KeyPath)
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
assert.GreaterOrEqual(t, triggered.Load(), int32(1))
|
||||
assert.Equal(t, LeashTriggered, leash.Status())
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestLeash_ReArmsWhenKeyRestored(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := testConfig(dir)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
|
||||
bus := alert.NewBus(10)
|
||||
leash := NewLeash(cfg, bus, nil, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go leash.Start(ctx)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
os.Remove(cfg.KeyPath)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
assert.Equal(t, LeashArmed, leash.Status())
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestLeash_SignalExtract(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := testConfig(dir)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
|
||||
var extracted atomic.Int32
|
||||
bus := alert.NewBus(10)
|
||||
leash := NewLeash(cfg, bus, func() { extracted.Add(1) }, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go leash.Start(ctx)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "signal_extract"), nil, 0o644)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
assert.GreaterOrEqual(t, extracted.Load(), int32(1))
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestLeash_SignalApoptosis(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := testConfig(dir)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
|
||||
var triggered atomic.Int32
|
||||
bus := alert.NewBus(10)
|
||||
leash := NewLeash(cfg, bus, nil, func() { triggered.Add(1) })
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go leash.Start(ctx)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "signal_apoptosis"), nil, 0o644)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
assert.GreaterOrEqual(t, triggered.Load(), int32(1))
|
||||
assert.Equal(t, LeashTriggered, leash.Status())
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestLeashStatus_String(t *testing.T) {
|
||||
assert.Equal(t, "DISARMED", LeashDisarmed.String())
|
||||
assert.Equal(t, "ARMED", LeashArmed.String())
|
||||
assert.Equal(t, "TRIGGERED", LeashTriggered.String())
|
||||
}
|
||||
|
||||
// --- v3.2 State Machine Tests ---
|
||||
|
||||
func TestParseMode(t *testing.T) {
|
||||
assert.Equal(t, ModeArmed, ParseMode("ARMED"))
|
||||
assert.Equal(t, ModeArmed, ParseMode("armed"))
|
||||
assert.Equal(t, ModeArmed, ParseMode("anything"))
|
||||
assert.Equal(t, ModeArmed, ParseMode(""))
|
||||
assert.Equal(t, ModeZeroG, ParseMode("ZERO-G"))
|
||||
assert.Equal(t, ModeZeroG, ParseMode("ZEROG"))
|
||||
assert.Equal(t, ModeZeroG, ParseMode("ZERO_G"))
|
||||
assert.Equal(t, ModeZeroG, ParseMode(" zero-g "))
|
||||
assert.Equal(t, ModeSafe, ParseMode("SAFE"))
|
||||
assert.Equal(t, ModeSafe, ParseMode("READ-ONLY"))
|
||||
assert.Equal(t, ModeSafe, ParseMode("READONLY"))
|
||||
}
|
||||
|
||||
func TestSystemMode_String(t *testing.T) {
|
||||
assert.Equal(t, "ARMED", ModeArmed.String())
|
||||
assert.Equal(t, "ZERO-G", ModeZeroG.String())
|
||||
assert.Equal(t, "SAFE", ModeSafe.String())
|
||||
}
|
||||
|
||||
func TestLeash_ModeDefault(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := testConfig(dir)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
|
||||
bus := alert.NewBus(10)
|
||||
leash := NewLeash(cfg, bus, nil, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go leash.Start(ctx)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
// No .sentinel_leash file → ModeArmed.
|
||||
assert.Equal(t, ModeArmed, leash.Mode())
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestLeash_ModeZeroG(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := testConfig(dir)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
os.WriteFile(cfg.LeashPath, []byte("ZERO-G"), 0o644)
|
||||
|
||||
bus := alert.NewBus(10)
|
||||
leash := NewLeash(cfg, bus, nil, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go leash.Start(ctx)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
assert.Equal(t, ModeZeroG, leash.Mode())
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestLeash_ModeSafe(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := testConfig(dir)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
os.WriteFile(cfg.LeashPath, []byte("SAFE"), 0o644)
|
||||
|
||||
bus := alert.NewBus(10)
|
||||
leash := NewLeash(cfg, bus, nil, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go leash.Start(ctx)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
assert.Equal(t, ModeSafe, leash.Mode())
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestLeash_ModeTransition(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := testConfig(dir)
|
||||
os.WriteFile(cfg.KeyPath, nil, 0o644)
|
||||
|
||||
var transitions atomic.Int32
|
||||
bus := alert.NewBus(10)
|
||||
leash := NewLeash(cfg, bus, nil, nil)
|
||||
leash.SetModeChangeCallback(func(m SystemMode) {
|
||||
transitions.Add(1)
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go leash.Start(ctx)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
|
||||
// ARMED → ZERO-G.
|
||||
require.NoError(t, os.WriteFile(cfg.LeashPath, []byte("ZERO-G"), 0o644))
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
assert.Equal(t, ModeZeroG, leash.Mode())
|
||||
|
||||
// ZERO-G → SAFE.
|
||||
require.NoError(t, os.WriteFile(cfg.LeashPath, []byte("SAFE"), 0o644))
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
assert.Equal(t, ModeSafe, leash.Mode())
|
||||
|
||||
assert.GreaterOrEqual(t, transitions.Load(), int32(2))
|
||||
cancel()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue