- audit JSON: IPMI sensor readings (ipmitool sensor) merged into hardware.sensors alongside lm-sensors data - audit JSON: IPMI SEL entries (ipmitool sel list) in hardware.event_logs with source "ipmi-sel" - audit JSON: dmesg error/warning lines in hardware.event_logs with source "dmesg" (filtered by error/warn/AER/Xid/NVRM/ECC/panic patterns) - support bundle: added ipmitool-sensor.txt, ipmitool-sel.txt, ipmitool-sel-time.txt to techdump - saa_dmi.go: fix dmiItemRE to accept SHN with parentheses (e.g. PS(4)LC for PSU fields) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
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"
|
|
}
|
|
}
|