155 lines
4.4 KiB
Go
155 lines
4.4 KiB
Go
package collector
|
|
|
|
import (
|
|
"bee/audit/internal/schema"
|
|
"log/slog"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// dmesg -T output: [Thu Jun 18 14:23:45 2026] message
|
|
// dmesg without -T: [ 123.456789] message
|
|
var dmesgTimestampRE = regexp.MustCompile(`^\[([^\]]+)\]\s*(.*)$`)
|
|
|
|
// Keywords that indicate an error or hardware problem worth capturing.
|
|
var dmesgErrorPatterns = []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)\berr(or)?\b`),
|
|
regexp.MustCompile(`(?i)\bfail(ed|ure)?\b`),
|
|
regexp.MustCompile(`(?i)\bfault\b`),
|
|
regexp.MustCompile(`(?i)\bwarn(ing)?\b`),
|
|
regexp.MustCompile(`(?i)\bAER\b`),
|
|
regexp.MustCompile(`(?i)\bXid\b`),
|
|
regexp.MustCompile(`(?i)\bNVRM\b`),
|
|
regexp.MustCompile(`(?i)\bpanic\b`),
|
|
regexp.MustCompile(`(?i)\bcorrected\b`),
|
|
regexp.MustCompile(`(?i)\buncorrect`),
|
|
regexp.MustCompile(`(?i)\bECC\b`),
|
|
regexp.MustCompile(`(?i)\btimeout\b`),
|
|
regexp.MustCompile(`(?i)\breset\b`),
|
|
regexp.MustCompile(`(?i)\bdead\b`),
|
|
regexp.MustCompile(`(?i)\bhang\b`),
|
|
regexp.MustCompile(`(?i)\bstall\b`),
|
|
regexp.MustCompile(`(?i)\bdisabled\b`),
|
|
}
|
|
|
|
// Boot-time messages below are noisy configuration/driver diagnostics, not
|
|
// hardware incidents. Raw dmesg remains in the bundle, but presenting each
|
|
// of them as Critical makes the event log unusable.
|
|
var dmesgIgnorePatterns = []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)bridge window .* failed to assign`),
|
|
regexp.MustCompile(`(?i)^NVRM: loading NVIDIA .* Kernel Module`),
|
|
regexp.MustCompile(`(?i)^NVRM: Persistence mode is deprecated`),
|
|
regexp.MustCompile(`(?i)^nvidia: module verification failed:.*tainting kernel`),
|
|
regexp.MustCompile(`(?i)^Yama: disabled by default`),
|
|
regexp.MustCompile(`(?i)^ERST: .*initialized`),
|
|
regexp.MustCompile(`(?i)iommu sva bind failed: -95`),
|
|
regexp.MustCompile(`(?i)gpuClearFbhubPoisonIntrForBug`),
|
|
}
|
|
|
|
// collectDmesgErrors runs `dmesg -T` (or `dmesg` without -T on failure) and
|
|
// returns only lines that match known error/warning patterns.
|
|
func collectDmesgErrors() []schema.HardwareEventLog {
|
|
out, err := exec.Command("dmesg", "-T").Output()
|
|
if err != nil || len(out) == 0 {
|
|
// Fallback: dmesg without human-readable timestamps
|
|
out, err = exec.Command("dmesg").Output()
|
|
if err != nil || len(out) == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
entries := parseDmesgErrors(string(out))
|
|
if len(entries) == 0 {
|
|
return nil
|
|
}
|
|
slog.Info("dmesg: collected error entries", "count", len(entries))
|
|
return entries
|
|
}
|
|
|
|
func parseDmesgErrors(output string) []schema.HardwareEventLog {
|
|
var entries []schema.HardwareEventLog
|
|
collectedAt := time.Now().UTC().Format(time.RFC3339)
|
|
|
|
for _, line := range strings.Split(output, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
var timestamp, message string
|
|
if m := dmesgTimestampRE.FindStringSubmatch(line); m != nil {
|
|
timestamp = strings.TrimSpace(m[1])
|
|
message = strings.TrimSpace(m[2])
|
|
} else {
|
|
message = line
|
|
}
|
|
|
|
if message == "" {
|
|
continue
|
|
}
|
|
if !matchesAny(message, dmesgErrorPatterns) {
|
|
continue
|
|
}
|
|
if matchesAny(message, dmesgIgnorePatterns) {
|
|
continue
|
|
}
|
|
|
|
severity := dmesgSeverity(message)
|
|
source := "dmesg"
|
|
|
|
var eventTime *string
|
|
if timestamp != "" {
|
|
t := timestamp
|
|
eventTime = &t
|
|
} else {
|
|
eventTime = &collectedAt
|
|
}
|
|
|
|
entries = append(entries, schema.HardwareEventLog{
|
|
Source: source,
|
|
EventTime: eventTime,
|
|
Severity: &severity,
|
|
Message: message,
|
|
})
|
|
}
|
|
return entries
|
|
}
|
|
|
|
func matchesAny(s string, patterns []*regexp.Regexp) bool {
|
|
for _, p := range patterns {
|
|
if p.MatchString(s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// dmesgSeverityCriticalPatterns use \b word boundaries, unlike a plain
|
|
// strings.Contains check, so common words that merely contain a keyword as a
|
|
// substring don't false-positive — e.g. "disabled by default" contains
|
|
// "fault" (de-fault), and "undead" would contain "dead".
|
|
var dmesgSeverityCriticalPatterns = []*regexp.Regexp{
|
|
regexp.MustCompile(`(?i)\bpanic\b`),
|
|
regexp.MustCompile(`(?i)\buncorrect`),
|
|
regexp.MustCompile(`(?i)\bxid\b`),
|
|
regexp.MustCompile(`(?i)\berror\b`),
|
|
regexp.MustCompile(`(?i)\bfault\b`),
|
|
regexp.MustCompile(`(?i)\bfail(ed|ure)?\b`),
|
|
regexp.MustCompile(`(?i)\bdead\b`),
|
|
regexp.MustCompile(`(?i)\bhang\b`),
|
|
}
|
|
|
|
func dmesgSeverity(msg string) string {
|
|
if sev, ok := XidSeverity(msg); ok {
|
|
if sev == "critical" {
|
|
return statusCritical
|
|
}
|
|
return statusWarning
|
|
}
|
|
if matchesAny(msg, dmesgSeverityCriticalPatterns) {
|
|
return statusCritical
|
|
}
|
|
return statusWarning
|
|
}
|