From 8c04a8eded4e7645f8001fdbbd90f9794add0332 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Wed, 21 Oct 2026 16:42:21 +0200 Subject: [PATCH] check_eventlog: add the "level" , "source" and "provider" filter translations to the WQL. adds support for string condition translation for "provider" --- Changes | 1 + docs/checks/commands/check_eventlog.md | 4 +- pkg/snclient/check_eventlog.go | 4 +- pkg/snclient/check_eventlog_query_windows.go | 87 +++++++++++++++++--- pkg/snclient/check_eventlog_windows_test.go | 41 +++++++-- 5 files changed, 115 insertions(+), 22 deletions(-) diff --git a/Changes b/Changes index 72829685..646eae29 100644 --- a/Changes +++ b/Changes @@ -3,6 +3,7 @@ This file documents the revision history for the SNClient agent. next: - update golang build chain to 1.27.3 - support multiline external commands and check_multi config + - check_eventlog: translate event type and source filter conditions into the WMI query 1.00 Fri Oct 16 16:47:03 CEST 2026 - add check_multi: execute multiple subchecks and evaluate the overall result diff --git a/docs/checks/commands/check_eventlog.md b/docs/checks/commands/check_eventlog.md index 2d19a482..19a33ba4 100755 --- a/docs/checks/commands/check_eventlog.md +++ b/docs/checks/commands/check_eventlog.md @@ -25,8 +25,8 @@ Basically, this check wraps this WMI query: See [here](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/eventlogprov/win32-ntlogevent) for a description of the provided fields. -Filter conditions on id are added to the WHERE clause of the WMI query when possible, -the filter is still applied to the returned events. +Filter conditions on id, level and source (or provider) are added to the WHERE clause +of the WMI query when possible, the filter is still applied to the returned events. - [Argument Defaults](#argument-defaults) - [Attributes](#attributes) diff --git a/pkg/snclient/check_eventlog.go b/pkg/snclient/check_eventlog.go index 3177e891..1c79d1e8 100644 --- a/pkg/snclient/check_eventlog.go +++ b/pkg/snclient/check_eventlog.go @@ -46,8 +46,8 @@ Basically, this check wraps this WMI query: See [here](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/eventlogprov/win32-ntlogevent) for a description of the provided fields. -Filter conditions on id are added to the WHERE clause of the WMI query when possible, -the filter is still applied to the returned events.`, +Filter conditions on id, level and source (or provider) are added to the WHERE clause +of the WMI query when possible, the filter is still applied to the returned events.`, implemented: Windows, result: &CheckResult{ State: CheckExitOK, diff --git a/pkg/snclient/check_eventlog_query_windows.go b/pkg/snclient/check_eventlog_query_windows.go index c81fb2f0..17965dc8 100644 --- a/pkg/snclient/check_eventlog_query_windows.go +++ b/pkg/snclient/check_eventlog_query_windows.go @@ -1,11 +1,17 @@ package snclient +import ( + "fmt" + "strings" + "unicode" +) + // The generic filter is applied to the events after the wmi query in CheckData.Finalize, -// so pushing filter conditions into the query is only an optimization. Only event ids are -// translated, all collected conditions are joined with OR, which selects a superset of the -// filter. Snclient filters the returned events again. +// so pushing filter conditions into the query is only an optimization. Event ids, event +// types and event sources are translated, all collected conditions are joined with OR, +// which selects a superset of the filter. Snclient filters the returned events again. -// buildFilterQueryWhereLine collects the event ids of the check filter and joins them +// buildFilterQueryWhereLine collects the translatable filter conditions and joins them // into a single WQL OR expression for the Win32_NTLogEvent query. // An empty string is returned when the filter cannot be translated safely. func (l *CheckEventlog) buildFilterQueryWhereLine(filter ConditionList) string { @@ -32,7 +38,7 @@ func (l *CheckEventlog) buildFilterQueryWhereLine(filter ConditionList) string { return nil } - where, ok := l.eventCodeConditionToWQL(cond) + where, ok := l.conditionLeafToWQL(cond) if ok { conditions = append(conditions, where...) @@ -58,13 +64,24 @@ func (l *CheckEventlog) buildFilterQueryWhereLine(filter ConditionList) string { return l.joinConditions(conditions) } -// eventCodeConditionToWQL translates the event ids of an "id" filter condition into EventCode conditions. -func (l *CheckEventlog) eventCodeConditionToWQL(cond *Condition) ([]string, bool) { - if cond.keyword != "id" { - return nil, false +// conditionLeafToWQL translates a single filter condition into WQL conditions. +func (l *CheckEventlog) conditionLeafToWQL(cond *Condition) ([]string, bool) { + switch cond.keyword { + case "id": + return l.eventCodeConditionToWQL(cond) + case "level": + // level is the lowercase Type property + return l.stringConditionToWQL("Type", cond) + case "source", "provider": + return l.stringConditionToWQL("SourceName", cond) } - values, ok := l.eventCodeValues(cond) + return nil, false +} + +// eventCodeConditionToWQL translates the event ids of an "id" filter condition into EventCode conditions. +func (l *CheckEventlog) eventCodeConditionToWQL(cond *Condition) ([]string, bool) { + values, ok := l.conditionValues(cond) if !ok { return nil, false } @@ -81,8 +98,41 @@ func (l *CheckEventlog) eventCodeConditionToWQL(cond *Condition) ([]string, bool return conditions, true } -// eventCodeValues returns the event ids of an "=" or "in" condition. -func (l *CheckEventlog) eventCodeValues(cond *Condition) ([]string, bool) { +// stringConditionToWQL translates equality and substring conditions into conditions for the given WQL string column. +func (l *CheckEventlog) stringConditionToWQL(column string, cond *Condition) ([]string, bool) { + switch cond.operator { + case Equal, InList: + values, ok := l.conditionValues(cond) + if !ok { + return nil, false + } + + wqlConditions := make([]string, 0, len(values)) + for _, value := range values { + // missing wmi columns are empty strings in the check, but they never match an empty wql string literal + if value == "" || checkWQLString(value) != nil { + return nil, false + } + wqlConditions = append(wqlConditions, fmt.Sprintf("%s = '%s'", column, value)) + } + + return wqlConditions, true + case Contains, ContainsCase: + value, ok := cond.value.(string) + // like is a substring match in the check, so it translates into: '%value%' + // wql cannot match missing values or the [ ] pattern characters with this translation. + if !ok || value == "" || strings.ContainsAny(value, "[]") || checkWQLString(value) != nil { + return nil, false + } + + return []string{fmt.Sprintf("%s LIKE '%%%s%%'", column, value)}, true + default: + return nil, false + } +} + +// conditionValues returns the values of an "=" or "in" condition. +func (l *CheckEventlog) conditionValues(cond *Condition) ([]string, bool) { switch cond.operator { case Equal: value, ok := cond.value.(string) @@ -102,3 +152,16 @@ func (l *CheckEventlog) eventCodeValues(cond *Condition) ([]string, bool) { return nil, false } } + +// checkWQLString rejects values that would break or inject into a WQL string literal. +func checkWQLString(value string) error { + // single quotes end the WQL string literal, backslashes are the WQL escape character + if strings.ContainsAny(value, `'\`) { + return fmt.Errorf("value %q contains a single quote or backslash", value) + } + if strings.ContainsFunc(value, func(r rune) bool { return !unicode.IsPrint(r) }) { + return fmt.Errorf("value %q contains non printable characters", value) + } + + return nil +} diff --git a/pkg/snclient/check_eventlog_windows_test.go b/pkg/snclient/check_eventlog_windows_test.go index dc5b27f9..dbd828dc 100644 --- a/pkg/snclient/check_eventlog_windows_test.go +++ b/pkg/snclient/check_eventlog_windows_test.go @@ -48,7 +48,7 @@ func TestCheckEventlogFilterQueryWhereLine(t *testing.T) { { test: "none filter combined with id and source", filters: []string{"none", "id in (823,824) and source like 'Firefox'"}, - expected: "(EventCode = 823 OR EventCode = 824)", + expected: "(EventCode = 823 OR EventCode = 824 OR SourceName LIKE '%Firefox%')", }, {test: "single event code", filters: []string{"id = 823"}, expected: "EventCode = 823"}, {test: "event code zero", filters: []string{"id = 0"}, expected: "EventCode = 0"}, @@ -62,16 +62,35 @@ func TestCheckEventlogFilterQueryWhereLine(t *testing.T) { {test: "event code out of range", filters: []string{"id = 65536"}, expected: ""}, {test: "event code not numeric", filters: []string{"id = 'abc'"}, expected: ""}, {test: "event code lower than", filters: []string{"id < 0"}, expected: ""}, - {test: "source only", filters: []string{"source like 'Firefox'"}, expected: ""}, + {test: "source equal", filters: []string{"source = 'MSSQL'"}, expected: "SourceName = 'MSSQL'"}, + {test: "provider equal", filters: []string{"provider = 'MSSQL'"}, expected: "SourceName = 'MSSQL'"}, + { + test: "source in list", + filters: []string{"source in ('MSSQL','SQL17')"}, + expected: "(SourceName = 'MSSQL' OR SourceName = 'SQL17')", + }, + {test: "source like", filters: []string{"source like 'Firefox'"}, expected: "SourceName LIKE '%Firefox%'"}, + {test: "source slike", filters: []string{"source slike 'Firefox'"}, expected: "SourceName LIKE '%Firefox%'"}, + {test: "source with backslash", filters: []string{`source = 'foo\bar'`}, expected: ""}, + {test: "source unlike", filters: []string{"source unlike 'Firefox'"}, expected: ""}, + {test: "level equal", filters: []string{"level = 'error'"}, expected: "Type = 'error'"}, + { + test: "level in list", + filters: []string{"level in ('warning','error','critical')"}, + expected: "(Type = 'warning' OR Type = 'error' OR Type = 'critical')", + }, + {test: "level like", filters: []string{"level like 'err'"}, expected: "Type LIKE '%err%'"}, + {test: "level not in", filters: []string{"level not in ('error')"}, expected: ""}, + {test: "level regex", filters: []string{"level ~ /err/"}, expected: ""}, {test: "written greater", filters: []string{"written > 0"}, expected: ""}, { test: "event code and condition not translatable", - filters: []string{"id = 1 and source like 'Firefox'"}, + filters: []string{"id = 1 and written > 0"}, expected: "EventCode = 1", }, { test: "event code or condition not translatable", - filters: []string{"id = 1 or source like 'Firefox'"}, + filters: []string{"id = 1 or written > 0"}, expected: "", }, { @@ -89,11 +108,21 @@ func TestCheckEventlogFilterQueryWhereLine(t *testing.T) { filters: []string{"id = 1", "id = 2"}, expected: "(EventCode = 1 OR EventCode = 2)", }, - {test: "multiple top level filters with one not translatable", filters: []string{"id = 1", "source like 'Firefox'"}, expected: ""}, + {test: "multiple top level filters with one not translatable", filters: []string{"id = 1", "written > 0"}, expected: ""}, + { + test: "source and level combined with id", + filters: []string{"id = 903 and source like 'MSSQL' and level = 'error'"}, + expected: "(EventCode = 903 OR SourceName LIKE '%MSSQL%' OR Type = 'error')", + }, + { + test: "message condition is ignored", + filters: []string{"id = 903 and message like 'foo'"}, + expected: "EventCode = 903", + }, { test: "mssql example", filters: []string{"id IN (823,824,825,5173) AND source like 'MSSQL' AND level IN ('critical','error')"}, - expected: "(EventCode = 823 OR EventCode = 824 OR EventCode = 825 OR EventCode = 5173)", + expected: "(EventCode = 823 OR EventCode = 824 OR EventCode = 825 OR EventCode = 5173 OR SourceName LIKE '%MSSQL%' OR Type = 'critical' OR Type = 'error')", }, }