mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-30 06:46:21 +02:00
Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates
This commit is contained in:
parent
694e32be26
commit
41cbfd6e0a
178 changed files with 36008 additions and 399 deletions
138
internal/infrastructure/guard/ebpf/soc_guard.c
Normal file
138
internal/infrastructure/guard/ebpf/soc_guard.c
Normal 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";
|
||||
417
internal/infrastructure/guard/guard.go
Normal file
417
internal/infrastructure/guard/guard.go
Normal 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
|
||||
}
|
||||
225
internal/infrastructure/guard/guard_test.go
Normal file
225
internal/infrastructure/guard/guard_test.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue