mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-27 05:16:22 +02:00
417 lines
10 KiB
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
|
|
}
|