package collector import ( "bee/audit/internal/schema" "fmt" "log/slog" "os/exec" "strings" ) // collectIPMISEL runs `ipmitool sel list` and returns parsed event log entries. // Returns nil if ipmitool is unavailable or the SEL is empty. func collectIPMISEL() []schema.HardwareEventLog { out, err := exec.Command("ipmitool", "sel", "list").Output() if err != nil || len(out) == 0 { return nil } entries := parseIPMISELOutput(string(out)) if len(entries) == 0 { return nil } slog.Info("ipmi sel: collected", "entries", len(entries)) return entries } // parseIPMISELOutput parses `ipmitool sel list` output. // Line format: ID | date | time | sensor | event description | direction // Example: 1 | 06/18/2026 | 14:23:45 | Temperature #0x30 | Upper Critical going high | Asserted func parseIPMISELOutput(output string) []schema.HardwareEventLog { var entries []schema.HardwareEventLog for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) if line == "" { continue } parts := strings.SplitN(line, "|", 6) if len(parts) < 5 { continue } id := strings.TrimSpace(parts[0]) date := strings.TrimSpace(parts[1]) timeStr := strings.TrimSpace(parts[2]) sensor := strings.TrimSpace(parts[3]) event := strings.TrimSpace(parts[4]) direction := "" if len(parts) == 6 { direction = strings.TrimSpace(parts[5]) } var eventTime *string if date != "" && timeStr != "" { t := fmt.Sprintf("%s %s", date, timeStr) eventTime = &t } message := event if direction != "" && strings.EqualFold(direction, "Deasserted") { message = event + " (Deasserted)" } severity := ipmiSELSeverity(event) isActive := !strings.EqualFold(direction, "Deasserted") entry := schema.HardwareEventLog{ Source: "ipmi-sel", EventTime: eventTime, Severity: &severity, MessageID: &id, Message: message, IsActive: &isActive, } if sensor != "" { entry.ComponentRef = &sensor } entries = append(entries, entry) } return entries } func ipmiSELSeverity(event string) string { lower := strings.ToLower(event) switch { case strings.Contains(lower, "critical") || strings.Contains(lower, "non-recoverable"): return statusCritical case strings.Contains(lower, "non-critical") || strings.Contains(lower, "warning") || strings.Contains(lower, "degraded"): return statusWarning default: return "info" } }