gomcp/internal/infrastructure/guard/guard.go

417 lines
10 KiB
Go

// Package guard implements the SEC-002 eBPF Runtime Guard policy engine.
//
// The guard monitors SOC processes at the kernel level using eBPF tracepoints
// and enforces per-process security policies defined in YAML.
//
// Modes of operation:
// - audit: log violations, never block
// - enforce: block violations via eBPF return codes
// - alert: send SOC events on violations
//
// On Windows/macOS: runs in audit-only mode using process monitoring fallback.
package guard
import (
"fmt"
"log/slog"
"os"
"strings"
"sync"
"time"
"gopkg.in/yaml.v3"
)
// Mode defines the guard operation mode.
type Mode string
const (
ModeAudit Mode = "audit" // Log only
ModeEnforce Mode = "enforce" // Block + log
ModeAlert Mode = "alert" // Alert only (SOC event)
)
// Policy is the top-level runtime guard policy.
type Policy struct {
Version string `yaml:"version"`
Mode Mode `yaml:"mode"`
Processes map[string]ProcessPolicy `yaml:"processes"`
Alerts AlertConfig `yaml:"alerts"`
}
// ProcessPolicy defines allowed/blocked behavior for a single process.
type ProcessPolicy struct {
Description string `yaml:"description"`
AllowedExec []string `yaml:"allowed_exec"`
BlockedSyscalls []string `yaml:"blocked_syscalls"`
AllowedFiles []string `yaml:"allowed_files"`
BlockedFiles []string `yaml:"blocked_files"`
AllowedNetwork []string `yaml:"allowed_network"`
BlockedNetwork []string `yaml:"blocked_network"`
MaxMemoryMB int `yaml:"max_memory_mb"`
MaxCPUPercent int `yaml:"max_cpu_percent"`
}
// AlertConfig defines alert routing.
type AlertConfig struct {
OnViolation []string `yaml:"on_violation"`
OnCritical []string `yaml:"on_critical"`
}
// Violation represents a detected policy violation.
type Violation struct {
Timestamp time.Time `json:"timestamp"`
ProcessName string `json:"process_name"`
PID int `json:"pid"`
Type string `json:"type"` // syscall, file, network, resource
Detail string `json:"detail"` // Specific violation description
Severity string `json:"severity"` // LOW, MEDIUM, HIGH, CRITICAL
Action string `json:"action"` // logged, blocked, alerted
PolicyMode Mode `json:"policy_mode"`
}
// ViolationHandler is called when a policy violation is detected.
type ViolationHandler func(v Violation)
// Guard is the runtime guard engine.
type Guard struct {
mu sync.RWMutex
policy *Policy
handlers []ViolationHandler
logger *slog.Logger
stats GuardStats
}
// GuardStats tracks guard operation metrics.
type GuardStats struct {
mu sync.Mutex
TotalEvents int64 `json:"total_events"`
Violations int64 `json:"violations"`
Blocked int64 `json:"blocked"`
ByProcess map[string]int64 `json:"by_process"`
ByType map[string]int64 `json:"by_type"`
StartedAt time.Time `json:"started_at"`
}
// New creates a new runtime guard with the given policy.
func New(policy *Policy) *Guard {
return &Guard{
policy: policy,
logger: slog.Default().With("component", "sec-002-guard"),
stats: GuardStats{
ByProcess: make(map[string]int64),
ByType: make(map[string]int64),
StartedAt: time.Now(),
},
}
}
// LoadPolicy reads and parses a YAML policy file.
func LoadPolicy(path string) (*Policy, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("guard: read policy %s: %w", path, err)
}
var policy Policy
if err := yaml.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("guard: parse policy %s: %w", path, err)
}
// Validate.
if policy.Version == "" {
policy.Version = "1.0"
}
if policy.Mode == "" {
policy.Mode = ModeAudit
}
if len(policy.Processes) == 0 {
return nil, fmt.Errorf("guard: policy has no process definitions")
}
return &policy, nil
}
// OnViolation registers a handler called on every violation.
func (g *Guard) OnViolation(h ViolationHandler) {
g.mu.Lock()
defer g.mu.Unlock()
g.handlers = append(g.handlers, h)
}
// CheckSyscall validates a syscall against the process policy.
func (g *Guard) CheckSyscall(processName string, pid int, syscall string) *Violation {
g.mu.RLock()
proc, exists := g.policy.Processes[processName]
mode := g.policy.Mode
g.mu.RUnlock()
if !exists {
return nil // Unknown process — not monitored.
}
for _, blocked := range proc.BlockedSyscalls {
if strings.EqualFold(blocked, syscall) {
v := Violation{
Timestamp: time.Now(),
ProcessName: processName,
PID: pid,
Type: "syscall",
Detail: fmt.Sprintf("blocked syscall: %s", syscall),
Severity: syscallSeverity(syscall),
PolicyMode: mode,
}
switch mode {
case ModeEnforce:
v.Action = "blocked"
case ModeAudit:
v.Action = "logged"
case ModeAlert:
v.Action = "alerted"
}
g.recordViolation(v)
return &v
}
}
return nil
}
// CheckFileAccess validates file access against the process policy.
func (g *Guard) CheckFileAccess(processName string, pid int, filepath string) *Violation {
g.mu.RLock()
proc, exists := g.policy.Processes[processName]
mode := g.policy.Mode
g.mu.RUnlock()
if !exists {
return nil
}
// Check blocked files first.
for _, pattern := range proc.BlockedFiles {
if matchGlob(pattern, filepath) {
v := Violation{
Timestamp: time.Now(),
ProcessName: processName,
PID: pid,
Type: "file",
Detail: fmt.Sprintf("blocked file access: %s (pattern: %s)", filepath, pattern),
Severity: "HIGH",
PolicyMode: mode,
}
if mode == ModeEnforce {
v.Action = "blocked"
} else {
v.Action = "logged"
}
g.recordViolation(v)
return &v
}
}
// Check if file is in allowed list.
allowed := false
for _, pattern := range proc.AllowedFiles {
if matchGlob(pattern, filepath) {
allowed = true
break
}
}
if !allowed && len(proc.AllowedFiles) > 0 {
v := Violation{
Timestamp: time.Now(),
ProcessName: processName,
PID: pid,
Type: "file",
Detail: fmt.Sprintf("unauthorized file access: %s", filepath),
Severity: "MEDIUM",
PolicyMode: mode,
}
if mode == ModeEnforce {
v.Action = "blocked"
} else {
v.Action = "logged"
}
g.recordViolation(v)
return &v
}
return nil
}
// CheckNetwork validates network access against the process policy.
func (g *Guard) CheckNetwork(processName string, pid int, addr string) *Violation {
g.mu.RLock()
proc, exists := g.policy.Processes[processName]
mode := g.policy.Mode
g.mu.RUnlock()
if !exists {
return nil
}
// soc-correlate should have NO network at all.
if len(proc.AllowedNetwork) == 0 {
v := Violation{
Timestamp: time.Now(),
ProcessName: processName,
PID: pid,
Type: "network",
Detail: fmt.Sprintf("network access denied (no network allowed): %s", addr),
Severity: "CRITICAL",
PolicyMode: mode,
}
if mode == ModeEnforce {
v.Action = "blocked"
} else {
v.Action = "logged"
}
g.recordViolation(v)
return &v
}
return nil
}
// CheckMemory validates memory usage against limits.
func (g *Guard) CheckMemory(processName string, pid int, memoryMB int) *Violation {
g.mu.RLock()
proc, exists := g.policy.Processes[processName]
mode := g.policy.Mode
g.mu.RUnlock()
if !exists || proc.MaxMemoryMB == 0 {
return nil
}
if memoryMB > proc.MaxMemoryMB {
v := Violation{
Timestamp: time.Now(),
ProcessName: processName,
PID: pid,
Type: "resource",
Detail: fmt.Sprintf("memory limit exceeded: %dMB > %dMB", memoryMB, proc.MaxMemoryMB),
Severity: "HIGH",
PolicyMode: mode,
}
if mode == ModeEnforce {
v.Action = "blocked"
} else {
v.Action = "logged"
}
g.recordViolation(v)
return &v
}
return nil
}
// Stats returns current guard statistics.
func (g *Guard) Stats() GuardStats {
g.stats.mu.Lock()
defer g.stats.mu.Unlock()
// Return a copy.
cp := GuardStats{
TotalEvents: g.stats.TotalEvents,
Violations: g.stats.Violations,
Blocked: g.stats.Blocked,
StartedAt: g.stats.StartedAt,
ByProcess: make(map[string]int64),
ByType: make(map[string]int64),
}
for k, v := range g.stats.ByProcess {
cp.ByProcess[k] = v
}
for k, v := range g.stats.ByType {
cp.ByType[k] = v
}
return cp
}
// Mode returns the current enforcement mode.
func (g *Guard) CurrentMode() Mode {
g.mu.RLock()
defer g.mu.RUnlock()
return g.policy.Mode
}
// SetMode changes the enforcement mode at runtime.
func (g *Guard) SetMode(mode Mode) {
g.mu.Lock()
defer g.mu.Unlock()
g.logger.Info("guard mode changed", "from", g.policy.Mode, "to", mode)
g.policy.Mode = mode
}
// recordViolation updates stats and notifies handlers.
func (g *Guard) recordViolation(v Violation) {
g.stats.mu.Lock()
g.stats.TotalEvents++
g.stats.Violations++
if v.Action == "blocked" {
g.stats.Blocked++
}
g.stats.ByProcess[v.ProcessName]++
g.stats.ByType[v.Type]++
g.stats.mu.Unlock()
g.logger.Warn("policy violation",
"process", v.ProcessName,
"pid", v.PID,
"type", v.Type,
"detail", v.Detail,
"severity", v.Severity,
"action", v.Action,
"mode", v.PolicyMode,
)
g.mu.RLock()
handlers := g.handlers
g.mu.RUnlock()
for _, h := range handlers {
h(v)
}
}
// --- Helpers ---
func syscallSeverity(name string) string {
critical := map[string]bool{
"ptrace": true, "process_vm_readv": true, "process_vm_writev": true,
"kexec_load": true, "init_module": true, "finit_module": true,
}
high := map[string]bool{
"execve": true, "fork": true, "clone": true, "clone3": true,
}
if critical[name] {
return "CRITICAL"
}
if high[name] {
return "HIGH"
}
return "MEDIUM"
}
func matchGlob(pattern, path string) bool {
// Simple glob matching: * matches any sequence.
if pattern == path {
return true
}
if strings.HasSuffix(pattern, "/*") {
prefix := strings.TrimSuffix(pattern, "/*")
return strings.HasPrefix(path, prefix)
}
if strings.HasSuffix(pattern, "*") {
prefix := strings.TrimSuffix(pattern, "*")
return strings.HasPrefix(path, prefix)
}
return false
}