Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/checks/commands/check_eventlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions pkg/snclient/check_eventlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
87 changes: 75 additions & 12 deletions pkg/snclient/check_eventlog_query_windows.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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...)

Expand All @@ -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
}
Expand All @@ -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)
Expand All @@ -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
}
41 changes: 35 additions & 6 deletions pkg/snclient/check_eventlog_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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: "",
},
{
Expand All @@ -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')",
},
}

Expand Down
Loading