mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-24 20:06:21 +02:00
190 lines
5.2 KiB
Go
190 lines
5.2 KiB
Go
// Package main provides the SENTINEL immune agent (SEC-002 eBPF Runtime Guard).
|
|
//
|
|
// The immune agent monitors SOC processes at the kernel level using eBPF
|
|
// tracepoints and enforces per-process security policies.
|
|
//
|
|
// On Linux: loads eBPF programs for syscall/file/network monitoring.
|
|
// On Windows/macOS: uses process monitoring fallback (polling /proc or WMI).
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run ./cmd/immune/ --policy deploy/policies/soc_runtime_policy.yaml
|
|
// SOC_GUARD_MODE=enforce go run ./cmd/immune/
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/syntrex-lab/gomcp/internal/infrastructure/guard"
|
|
"github.com/syntrex-lab/gomcp/internal/infrastructure/logging"
|
|
)
|
|
|
|
func main() {
|
|
// SEC-003: Panic recovery.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
buf := make([]byte, 4096)
|
|
n := runtime.Stack(buf, false)
|
|
fmt.Fprintf(os.Stderr, "IMMUNE FATAL PANIC: %v\n%s\n", r, buf[:n])
|
|
os.Exit(2)
|
|
}
|
|
}()
|
|
|
|
logger := logging.New(env("SOC_LOG_FORMAT", "text"), env("SOC_LOG_LEVEL", "info"))
|
|
slog.SetDefault(logger)
|
|
|
|
// SEC-003: Memory safety — immune agent uses minimal RAM.
|
|
if os.Getenv("GOMEMLIMIT") == "" {
|
|
debug.SetMemoryLimit(128 * 1024 * 1024) // 128 MiB
|
|
}
|
|
|
|
policyPath := env("SOC_GUARD_POLICY", "deploy/policies/soc_runtime_policy.yaml")
|
|
port, _ := strconv.Atoi(env("SOC_IMMUNE_PORT", "9760"))
|
|
|
|
logger.Info("starting SENTINEL immune agent (SEC-002 eBPF Runtime Guard)",
|
|
"policy", policyPath,
|
|
"port", port,
|
|
"os", runtime.GOOS,
|
|
)
|
|
|
|
// Load policy.
|
|
policy, err := guard.LoadPolicy(policyPath)
|
|
if err != nil {
|
|
logger.Error("failed to load policy", "path", policyPath, "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Override mode from env if set.
|
|
if modeOverride := os.Getenv("SOC_GUARD_MODE"); modeOverride != "" {
|
|
policy.Mode = guard.Mode(modeOverride)
|
|
logger.Info("mode overridden via env", "mode", policy.Mode)
|
|
}
|
|
|
|
g := guard.New(policy)
|
|
|
|
// Register violation handler → forward to SOC.
|
|
g.OnViolation(func(v guard.Violation) {
|
|
logger.Warn("GUARD VIOLATION",
|
|
"process", v.ProcessName,
|
|
"pid", v.PID,
|
|
"type", v.Type,
|
|
"detail", v.Detail,
|
|
"severity", v.Severity,
|
|
"action", v.Action,
|
|
)
|
|
// TODO: Forward to SOC via HTTP or IPC.
|
|
})
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
// Start platform-specific monitoring.
|
|
go startProcessMonitor(ctx, g, logger)
|
|
|
|
// HTTP status endpoint for health checks and stats.
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"status": "healthy",
|
|
"mode": g.CurrentMode(),
|
|
"os": runtime.GOOS,
|
|
})
|
|
})
|
|
|
|
mux.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(g.Stats())
|
|
})
|
|
|
|
mux.HandleFunc("/mode", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodPost {
|
|
var req struct {
|
|
Mode string `json:"mode"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
slog.Error("invalid mode request", "error", err)
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
g.SetMode(guard.Mode(req.Mode))
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"mode": string(g.CurrentMode())})
|
|
})
|
|
|
|
srv := &http.Server{
|
|
Addr: fmt.Sprintf(":%d", port),
|
|
Handler: mux,
|
|
ReadTimeout: 5 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
logger.Info("immune HTTP status endpoint ready", "port", port)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
logger.Error("HTTP server failed", "error", err)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
logger.Info("immune shutting down")
|
|
srv.Shutdown(context.Background())
|
|
}
|
|
|
|
// startProcessMonitor runs the platform-specific process monitoring loop.
|
|
// On Linux: would attach eBPF programs and read from ringbuf.
|
|
// On Windows/macOS: polls process list for anomalies.
|
|
func startProcessMonitor(ctx context.Context, g *guard.Guard, logger *slog.Logger) {
|
|
logger.Info("starting process monitor",
|
|
"platform", runtime.GOOS,
|
|
"note", "using polling fallback (eBPF requires Linux)",
|
|
)
|
|
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
// Polling fallback: check process resource usage.
|
|
// On Linux with eBPF: this would be event-driven from ringbuf.
|
|
checkProcessResources(g, logger)
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkProcessResources polls OS for SOC process resource usage.
|
|
func checkProcessResources(g *guard.Guard, logger *slog.Logger) {
|
|
// This is a simplified polling fallback.
|
|
// On Linux with eBPF loaded, violations come from kernel tracepoints instead.
|
|
//
|
|
// In production:
|
|
// - Linux: bpf_ringbuf_poll() for real-time syscall events
|
|
// - Windows: ETW (Event Tracing for Windows) or WMI queries
|
|
// - macOS: Endpoint Security framework
|
|
logger.Debug("process resource check (polling fallback)")
|
|
}
|
|
|
|
func env(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|