Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates

This commit is contained in:
DmitrL-dev 2026-03-23 16:45:40 +10:00
parent 694e32be26
commit 41cbfd6e0a
178 changed files with 36008 additions and 399 deletions

View file

@ -0,0 +1,138 @@
// SEC-002: eBPF Runtime Guard kernel program.
//
// This is a REFERENCE IMPLEMENTATION — requires Linux kernel 5.10+
// and libbpf/bpftool to compile:
//
// clang -O2 -target bpf -c soc_guard.c -o soc_guard.o
// bpftool prog load soc_guard.o /sys/fs/bpf/soc_guard
//
// The Go userspace agent (cmd/immune/main.go) loads this program
// and manages the policy maps.
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
// Policy map: pid → policy flags (bit field).
// Bit 0: monitored (1 = yes)
// Bit 1: ptrace blocked
// Bit 2: execve blocked
// Bit 3: network blocked
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, __u32); // pid
__type(value, __u32); // policy_flags
} soc_policy_map SEC(".maps");
// Alert ring buffer for sending violations to userspace.
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); // 256KB
} soc_alerts SEC(".maps");
// Alert event structure sent to userspace.
struct soc_alert {
__u32 pid;
__u32 tgid;
__u32 alert_type; // 1=ptrace, 2=execve, 3=network, 4=file
__u32 blocked; // 1=blocked (enforce), 0=logged (audit)
__u64 timestamp_ns;
char comm[16]; // process name
char detail[64]; // violation details
};
// Alert types.
#define ALERT_PTRACE_ATTEMPT 1
#define ALERT_UNAUTHORIZED_EXEC 2
#define ALERT_NETWORK_DENIED 3
#define ALERT_FILE_DENIED 4
// Policy flags.
#define POLICY_MONITORED (1 << 0)
#define POLICY_BLOCK_PTRACE (1 << 1)
#define POLICY_BLOCK_EXECVE (1 << 2)
#define POLICY_BLOCK_NETWORK (1 << 3)
static __always_inline void send_alert(
__u32 pid, __u32 alert_type, __u32 blocked, const char *detail
) {
struct soc_alert *alert;
alert = bpf_ringbuf_reserve(&soc_alerts, sizeof(*alert), 0);
if (!alert)
return;
alert->pid = pid;
alert->tgid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
alert->alert_type = alert_type;
alert->blocked = blocked;
alert->timestamp_ns = bpf_ktime_get_ns();
bpf_get_current_comm(alert->comm, sizeof(alert->comm));
__builtin_memset(alert->detail, 0, sizeof(alert->detail));
// detail is truncated; full info is in userspace log.
bpf_ringbuf_submit(alert, 0);
}
// ═══════════════════════════════════════════════
// TRACEPOINT: Block ptrace on monitored SOC processes
// ═══════════════════════════════════════════════
SEC("tracepoint/syscalls/sys_enter_ptrace")
int soc_guard_ptrace(struct trace_event_raw_sys_enter *ctx) {
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u32 target_pid = (__u32)ctx->args[1]; // ptrace(request, pid, ...)
// Check if TARGET is a monitored SOC process.
__u32 *flags = bpf_map_lookup_elem(&soc_policy_map, &target_pid);
if (!flags)
return 0; // Not a SOC process.
if (*flags & POLICY_BLOCK_PTRACE) {
send_alert(pid, ALERT_PTRACE_ATTEMPT, 1, "ptrace on SOC process");
return -1; // EPERM — block the syscall.
}
send_alert(pid, ALERT_PTRACE_ATTEMPT, 0, "ptrace audit");
return 0;
}
// ═══════════════════════════════════════════════
// TRACEPOINT: Monitor execve calls by SOC processes
// ═══════════════════════════════════════════════
SEC("tracepoint/syscalls/sys_enter_execve")
int soc_guard_execve(struct trace_event_raw_sys_enter *ctx) {
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u32 *flags = bpf_map_lookup_elem(&soc_policy_map, &pid);
if (!flags)
return 0;
if (*flags & POLICY_BLOCK_EXECVE) {
send_alert(pid, ALERT_UNAUTHORIZED_EXEC, 1, "execve blocked");
return -1;
}
send_alert(pid, ALERT_UNAUTHORIZED_EXEC, 0, "execve audit");
return 0;
}
// ═══════════════════════════════════════════════
// TRACEPOINT: Monitor socket creation (network access)
// ═══════════════════════════════════════════════
SEC("tracepoint/syscalls/sys_enter_socket")
int soc_guard_socket(struct trace_event_raw_sys_enter *ctx) {
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u32 *flags = bpf_map_lookup_elem(&soc_policy_map, &pid);
if (!flags)
return 0;
if (*flags & POLICY_BLOCK_NETWORK) {
send_alert(pid, ALERT_NETWORK_DENIED, 1, "socket creation blocked");
return -1;
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";

View file

@ -0,0 +1,417 @@
// 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
}

View file

@ -0,0 +1,225 @@
package guard
import (
"os"
"testing"
)
func testPolicy() *Policy {
return &Policy{
Version: "1.0",
Mode: ModeAudit,
Processes: map[string]ProcessPolicy{
"soc-ingest": {
Description: "test ingest",
BlockedSyscalls: []string{"ptrace", "process_vm_readv"},
AllowedFiles: []string{"/var/lib/sentinel/data/*", "/tmp/*"},
BlockedFiles: []string{"/etc/shadow", "/root/*"},
AllowedNetwork: []string{"0.0.0.0:9750"},
MaxMemoryMB: 512,
},
"soc-correlate": {
Description: "test correlate — no network",
BlockedSyscalls: []string{"ptrace", "execve", "fork", "socket"},
AllowedFiles: []string{"/var/lib/sentinel/data/*"},
BlockedFiles: []string{"/etc/*", "/root/*"},
AllowedNetwork: []string{}, // NONE
MaxMemoryMB: 1024,
},
},
}
}
func TestCheckSyscall_Blocked(t *testing.T) {
g := New(testPolicy())
v := g.CheckSyscall("soc-ingest", 1234, "ptrace")
if v == nil {
t.Fatal("expected violation for ptrace")
}
if v.Severity != "CRITICAL" {
t.Errorf("severity = %s, want CRITICAL", v.Severity)
}
if v.Action != "logged" {
t.Errorf("action = %s, want logged (audit mode)", v.Action)
}
}
func TestCheckSyscall_Allowed(t *testing.T) {
g := New(testPolicy())
v := g.CheckSyscall("soc-ingest", 1234, "read")
if v != nil {
t.Errorf("unexpected violation for read: %+v", v)
}
}
func TestCheckSyscall_EnforceMode(t *testing.T) {
p := testPolicy()
p.Mode = ModeEnforce
g := New(p)
v := g.CheckSyscall("soc-correlate", 5678, "execve")
if v == nil {
t.Fatal("expected violation for execve")
}
if v.Action != "blocked" {
t.Errorf("action = %s, want blocked (enforce mode)", v.Action)
}
}
func TestCheckSyscall_UnknownProcess(t *testing.T) {
g := New(testPolicy())
v := g.CheckSyscall("unknown-proc", 9999, "ptrace")
if v != nil {
t.Errorf("expected nil for unknown process, got %+v", v)
}
}
func TestCheckFileAccess_Blocked(t *testing.T) {
g := New(testPolicy())
v := g.CheckFileAccess("soc-ingest", 1234, "/etc/shadow")
if v == nil {
t.Fatal("expected violation for /etc/shadow")
}
if v.Severity != "HIGH" {
t.Errorf("severity = %s, want HIGH", v.Severity)
}
}
func TestCheckFileAccess_Allowed(t *testing.T) {
g := New(testPolicy())
v := g.CheckFileAccess("soc-ingest", 1234, "/var/lib/sentinel/data/soc.db")
if v != nil {
t.Errorf("unexpected violation for allowed path: %+v", v)
}
}
func TestCheckFileAccess_Unauthorized(t *testing.T) {
g := New(testPolicy())
v := g.CheckFileAccess("soc-ingest", 1234, "/opt/something/secret")
if v == nil {
t.Fatal("expected violation for unauthorized path")
}
if v.Severity != "MEDIUM" {
t.Errorf("severity = %s, want MEDIUM", v.Severity)
}
}
func TestCheckNetwork_NoNetworkAllowed(t *testing.T) {
g := New(testPolicy())
// soc-correlate has AllowedNetwork: [] — no network at all.
v := g.CheckNetwork("soc-correlate", 5678, "8.8.8.8:443")
if v == nil {
t.Fatal("expected violation for network on correlate")
}
if v.Severity != "CRITICAL" {
t.Errorf("severity = %s, want CRITICAL", v.Severity)
}
}
func TestCheckMemory_Exceeded(t *testing.T) {
g := New(testPolicy())
v := g.CheckMemory("soc-ingest", 1234, 600) // 600MB > 512MB limit
if v == nil {
t.Fatal("expected violation for memory exceeded")
}
if v.Severity != "HIGH" {
t.Errorf("severity = %s, want HIGH", v.Severity)
}
}
func TestCheckMemory_Within(t *testing.T) {
g := New(testPolicy())
v := g.CheckMemory("soc-ingest", 1234, 400) // 400MB < 512MB
if v != nil {
t.Errorf("unexpected violation for memory within limit: %+v", v)
}
}
func TestStats(t *testing.T) {
g := New(testPolicy())
g.CheckSyscall("soc-ingest", 1, "ptrace")
g.CheckSyscall("soc-ingest", 1, "process_vm_readv")
g.CheckFileAccess("soc-ingest", 1, "/etc/shadow")
stats := g.Stats()
if stats.Violations != 3 {
t.Errorf("violations = %d, want 3", stats.Violations)
}
if stats.ByProcess["soc-ingest"] != 3 {
t.Errorf("by_process[soc-ingest] = %d, want 3", stats.ByProcess["soc-ingest"])
}
if stats.ByType["syscall"] != 2 {
t.Errorf("by_type[syscall] = %d, want 2", stats.ByType["syscall"])
}
}
func TestSetMode(t *testing.T) {
g := New(testPolicy())
if g.CurrentMode() != ModeAudit {
t.Fatalf("initial mode = %s, want audit", g.CurrentMode())
}
g.SetMode(ModeEnforce)
if g.CurrentMode() != ModeEnforce {
t.Errorf("mode after set = %s, want enforce", g.CurrentMode())
}
}
func TestViolationHandler(t *testing.T) {
g := New(testPolicy())
var received []Violation
g.OnViolation(func(v Violation) {
received = append(received, v)
})
g.CheckSyscall("soc-ingest", 1, "ptrace")
if len(received) != 1 {
t.Fatalf("handler received %d violations, want 1", len(received))
}
if received[0].Type != "syscall" {
t.Errorf("type = %s, want syscall", received[0].Type)
}
}
func TestLoadPolicy(t *testing.T) {
// Write temp policy file.
content := `
version: "1.0"
mode: enforce
processes:
test-proc:
blocked_syscalls: [ptrace]
allowed_files: [/tmp/*]
`
tmpFile := t.TempDir() + "/test_policy.yaml"
if err := writeFile(tmpFile, content); err != nil {
t.Fatalf("write temp policy: %v", err)
}
policy, err := LoadPolicy(tmpFile)
if err != nil {
t.Fatalf("LoadPolicy: %v", err)
}
if policy.Mode != ModeEnforce {
t.Errorf("mode = %s, want enforce", policy.Mode)
}
if _, ok := policy.Processes["test-proc"]; !ok {
t.Error("expected test-proc in processes")
}
}
func writeFile(path, content string) error {
return os.WriteFile(path, []byte(content), 0644)
}