// eBPF violation event watcher. // // The Warding monitors security violations from the eBPF Tomb Guard // program via a perf event buffer. When the eBPF LSM hook detects a // write attempt on a protected path (Tomb, State DB), it emits a // violation event that this code receives and converts into Warding // Alarms. // // This replaces the former AppArmor / auditd log watcher which parsed // /var/log/audit/audit.log for "apparmor=DENIED" strings. The eBPF // approach is faster (in-kernel, no log parsing), more precise // (per-syscall, not per-audit-message), and works uniformly across // LXC, Firecracker, and Podman containers. // // Architecture: // // Kernel (eBPF LSM) Userspace (Go) // ───────────────── ─────────────── // tomb_guard.bpf.o audit.go // ├─ file_permission hook ───────► WatchViolations() // ├─ inode_permission hook ├─ perf event reader // └─ trace_execve audit trail ├─ → Alarm{Severity: "taint"} // └─ → eventbus.Publish("warding") package warding import ( "fmt" "os" "strings" "time" ) // WatchViolations starts watching eBPF violation events from the Tomb Guard // program. Each violation becomes an Alarm of severity "taint" — the Cockpit // Threat Map renders them in real time via WebSocket. // // This method replaces the former WatchAuditLogs which parsed AppArmor log // entries. It uses the eBPF enforcer's perf buffer for real-time event // delivery, eliminating the polling delay and log-rotation edge cases. // // Blocks until `stop` is closed. func (w *Warding) WatchViolations(stop <-chan struct{}) { if w.ebpf == nil { // eBPF not available — fall back to no-op with a warning. w.soundAlarm(Alarm{ Severity: "taint", Message: "eBPF enforcer not loaded — Tomb Guard violation monitoring unavailable", }) <-stop return } // Forward eBPF violations to Warding alarms. handler := func(v EBPFViolation) { // Only report write-related violations (the eBPF program sends // both exec audit events and file access violations). // Skip if NOT a write (bit 1 of AccessMask) AND NOT execve (syscall 59). if v.AccessMask&2 == 0 && v.SyscallNr != 59 { return } // Filter: only care about sorcery-related paths. if !strings.Contains(v.Path, "sorcery") && !strings.Contains(v.Path, "tomb") { return } severity := "taint" if v.SyscallNr == 59 { severity = "audit" // exec events are audit, not taint } w.soundAlarm(Alarm{ Severity: severity, Message: fmt.Sprintf("eBPF DENIED [%s] pid=%d comm=%s path=%s", syscallName(v.SyscallNr), v.PID, v.Comm, v.Path), Node: fmt.Sprintf("pid:%d", v.PID), }) } if err := w.ebpf.WatchViolations(handler); err != nil { w.soundAlarm(Alarm{ Severity: "taint", Message: "eBPF violation watch failed: " + err.Error(), }) <-stop return } <-stop w.ebpf.StopWatching() } // WatchAuditLogs is preserved for backward compatibility and as a fallback // when eBPF is not available. It tails the audit log for any remaining // kernel-level denials that the eBPF program might not catch (e.g., from // other MAC systems still on the host). // // Deprecated: Use WatchViolations for eBPF-based monitoring. func (w *Warding) WatchAuditLogs(path string, startingPos int64, stop <-chan struct{}) { if path == "" { path = "/var/log/audit/audit.log" } pos := startingPos ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-stop: return case <-ticker.C: newPos, events := tailAuditLog(path, pos) pos = newPos for _, line := range events { if !strings.Contains(line, "DENIED") && !strings.Contains(line, "denied") { continue } if !strings.Contains(line, "/var/lib/sorcery") { continue } source := "kernel" if strings.Contains(line, "apparmor") { source = "AppArmor (legacy)" } else if strings.Contains(line, "selinux") { source = "SELinux" } else if strings.Contains(line, "ebpf") { source = "eBPF" } w.soundAlarm(Alarm{ Severity: "taint", Message: fmt.Sprintf("%s DENIED: %s", source, extractAuditField(line, "name")), Node: extractAuditField(line, "comm"), }) } } } } // tailAuditLog reads new bytes from an audit log file starting at pos. func tailAuditLog(path string, pos int64) (int64, []string) { f, err := os.Open(path) if err != nil { return pos, nil } defer f.Close() info, err := f.Stat() if err != nil { return pos, nil } if info.Size() < pos { // File was rotated — start from the beginning. pos = 0 } if info.Size() == pos { return pos, nil } if _, err := f.Seek(pos, 0); err != nil { return pos, nil } buf := make([]byte, info.Size()-pos) n, err := f.Read(buf) if err != nil && n == 0 { return pos, nil } newPos := pos + int64(n) lines := strings.Split(string(buf[:n]), "\n") // Remove trailing empty string from split. if len(lines) > 0 && lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] } return newPos, lines } // syscallNameMap maps common syscall numbers to names. // Package-level to avoid realloc on every call. var syscallNameMap = map[uint32]string{ 2: "open", 3: "close", 59: "execve", 257: "openat", 262: "newfstatat", 281: "execveat", } // syscallName returns a human-readable name for common syscall numbers. func syscallName(nr uint32) string { if name, ok := syscallNameMap[nr]; ok { return name } return fmt.Sprintf("sys_%d", nr) } // EBPFViolation is an alias for the eBPF package's Violation type, // used to decouple the warding package from the eBPF package at the // type level while keeping the interface clean. type EBPFViolation = ebpfViolation // ebpfViolation mirrors the eBPF Violation struct. // In production this would reference pkg/warding/ebpf.Violation directly, // but we define it here to avoid a build dependency on the generated code. type ebpfViolation struct { PID uint32 TID uint32 UID uint32 GID uint32 PPID uint32 SyscallNr uint32 Comm string Path string AccessMask int32 } // extractAuditField extracts a key=value field from an audit log line. // Falls back to "unknown" if the field is not found. func extractAuditField(line, key string) string { needle := key + "=" start := strings.Index(line, needle) if start == -1 { return "unknown" } val := line[start+len(needle):] if end := strings.Index(val, " "); end != -1 { val = val[:end] } // Strip surrounding quotes. val = strings.Trim(val, `"'`) return val }