diff --git a/.golangci.yml b/.golangci.yml index a96a62e6..6f9d5b72 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -75,9 +75,13 @@ issues: - "Magic number: 15, in" - "Magic number: 16, in" - "Magic number: 20, in" + - "Magic number: 22, in" + - "Magic number: 23, in" - "Magic number: 24, in" + - "Magic number: 31, in" - "Magic number: 32, in" - "Magic number: 50, in" + - "Magic number: 59, in" - "Magic number: 60, in" - "Magic number: 64, in" - "Magic number: 100, in" diff --git a/Changes b/Changes index 0b3e6b78..e58da041 100644 --- a/Changes +++ b/Changes @@ -3,6 +3,7 @@ This file documents the revision history for the SNClient+ agent. next: - update windows exporter to 0.29.0 - wmi: always set en_US language in query (#156) + - check_eventlog: fix time offset parsing (#157) 0.27 Mon Sep 2 19:31:14 CEST 2024 - do not use empty-state if warn/crit conditions contain check on 'count' diff --git a/pkg/eventlog/eventlog_windows.go b/pkg/eventlog/eventlog_windows.go index 4e0e3f89..e94e40f8 100644 --- a/pkg/eventlog/eventlog_windows.go +++ b/pkg/eventlog/eventlog_windows.go @@ -8,7 +8,7 @@ import ( ) const ( - WMIDateFormat = "20060102150405.000000-070" + WMIDateFormat = "20060102150405.000000" ) type EventLog struct { @@ -47,6 +47,24 @@ type Event struct { func GetLog(file string, newerThan time.Time) ([]Event, error) { messages := []Event{} + + // Format the time without the timezone offset + formattedTime := newerThan.Format(WMIDateFormat) + + // Get the timezone offset in seconds and convert it to minutes + _, offsetSeconds := newerThan.Zone() + offsetMinutes := offsetSeconds / 60 + + // Determine the sign of the offset + offsetSign := "+" + if offsetMinutes < 0 { + offsetSign = "-" + offsetMinutes = -offsetMinutes // Make offsetMinutes positive for formatting + } + + // append the offset with three digits and leading zeros + wmiFormattedTime := fmt.Sprintf("%s%s%03d", formattedTime, offsetSign, offsetMinutes) + query := fmt.Sprintf(` SELECT ComputerName, @@ -65,7 +83,7 @@ func GetLog(file string, newerThan time.Time) ([]Event, error) { WHERE Logfile='%s' AND TimeGenerated >= '%s' - `, file, newerThan.Format(WMIDateFormat)) + `, file, wmiFormattedTime) err := wmi.QueryDefaultRetry(query, &messages) if err != nil { return nil, fmt.Errorf("wmi query failed: %s", err.Error()) diff --git a/pkg/snclient/check_eventlog_windows.go b/pkg/snclient/check_eventlog_windows.go index b3b73b02..752ebbe5 100644 --- a/pkg/snclient/check_eventlog_windows.go +++ b/pkg/snclient/check_eventlog_windows.go @@ -3,6 +3,7 @@ package snclient import ( "context" "fmt" + "strconv" "strings" "time" @@ -57,7 +58,7 @@ func (l *CheckEventlog) Check(_ context.Context, _ *Agent, check *CheckData, _ [ for i := range fileEvent { event := fileEvent[i] - timeWritten, _ := time.Parse(eventlog.WMIDateFormat, event.TimeWritten) + timeWritten, _ := l.ParseWMIDateTime(event.TimeWritten) message := event.Message if l.truncateMessage > 0 && len(event.Message) > l.truncateMessage { message = event.Message[:l.truncateMessage] @@ -96,3 +97,98 @@ func (l *CheckEventlog) Check(_ context.Context, _ *Agent, check *CheckData, _ [ return check.Finalize() } + +// ParseWMIDateTime parses a WMI datetime string into a time.Time object. +// returns parsed time or an error +func (l *CheckEventlog) ParseWMIDateTime(wmiDateTime string) (time.Time, error) { + // Check if the string has at least 22 characters to avoid slicing errors + if len(wmiDateTime) < 22 { + return time.Time{}, fmt.Errorf("invalid WMI datetime string, must be at least 22 characters long") + } + + // Extract the date and time components + yearStr := wmiDateTime[0:4] + monthStr := wmiDateTime[4:6] + dayStr := wmiDateTime[6:8] + hourStr := wmiDateTime[8:10] + minuteStr := wmiDateTime[10:12] + secondStr := wmiDateTime[12:14] + microsecStr := wmiDateTime[15:21] // Skipping the dot at position 14 + offsetSign := wmiDateTime[21:22] + offsetStr := wmiDateTime[22:] + + year, err := strconv.Atoi(yearStr) + if err2 := l.checkRange("year", year, err, -1, -1); err2 != nil { + return time.Time{}, err2 + } + + month, err := strconv.Atoi(monthStr) + if err2 := l.checkRange("month", month, err, 1, 12); err2 != nil { + return time.Time{}, err2 + } + + day, err := strconv.Atoi(dayStr) + if err2 := l.checkRange("day", day, err, 1, 31); err2 != nil { + return time.Time{}, err2 + } + + hour, err := strconv.Atoi(hourStr) + if err2 := l.checkRange("hour", hour, err, 0, 23); err2 != nil { + return time.Time{}, err2 + } + + minute, err := strconv.Atoi(minuteStr) + if err2 := l.checkRange("minute", minute, err, 0, 59); err2 != nil { + return time.Time{}, err2 + } + + second, err := strconv.Atoi(secondStr) + if err2 := l.checkRange("second", second, err, 0, 59); err2 != nil { + return time.Time{}, err2 + } + + microsec, err := strconv.Atoi(microsecStr) + if err2 := l.checkRange("microsecond", microsec, err, -1, -1); err2 != nil { + return time.Time{}, err2 + } + + offsetMinutes, err := strconv.Atoi(offsetStr) + if err2 := l.checkRange("offset", offsetMinutes, err, -1, -1); err2 != nil { + return time.Time{}, err2 + } + + // Apply the sign to the offset + if offsetSign == "-" { + offsetMinutes = -offsetMinutes + } else if offsetSign != "+" { + // Invalid sign, return current time + return time.Time{}, fmt.Errorf("invalid offset sign, must be + or -") + } + + // Convert offset from minutes to seconds + offsetSeconds := offsetMinutes * 60 + + // Create a fixed time zone based on the offset + loc := time.FixedZone("WMI", offsetSeconds) + + // Construct the time.Time object + t := time.Date(year, time.Month(month), day, hour, minute, second, microsec*1000, loc) + + return t, nil +} + +func (l *CheckEventlog) checkRange(name string, value int, err error, minVal, maxVal int) error { + if err != nil { + return fmt.Errorf("invalid %s: %s", name, err.Error()) + } + + if minVal != -1 && value < minVal { + return fmt.Errorf("%s out of range", name) + } + + if maxVal != -1 && value > maxVal { + return fmt.Errorf("%s out of range", name) + } + + return nil +}