From f331f6a5f58125f170358e874ee6fdab75011e1a Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 23 Jun 2026 12:52:34 +0200 Subject: [PATCH 1/8] check_tasksched: misc improvements add hidden argument to check, this is used while discovering tasks in the embedded powershell script directly. add debug to stderr calls for the folder,title, recursive and hidden parameters/args in the script add logic to remove trailing backslash in the folder path, is only removed if its not root path . this is needed due to new script using scheduler.service com tasks change the working_dir attribute to working_directory to match with existing check definitions --- pkg/snclient/check_tasksched.go | 6 +++++- pkg/snclient/check_tasksched_windows.go | 18 +++++++++++++++++- .../embed/scripts/windows/scheduled_tasks.ps1 | 19 ++++++++++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index fd0beea8..121b7247 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -14,12 +14,14 @@ type CheckTasksched struct { TaskTitle string Folder string Recursive bool + Hidden bool } const ( CheckTaskschedDefaultTaskTitle string = "*" CheckTaskschedDefaultFolder string = "\\" CheckTaskschedDefaultRecursive bool = true + CheckTaskschedDefaultHidden bool = false ) func NewCheckTasksched() CheckHandler { @@ -27,6 +29,7 @@ func NewCheckTasksched() CheckHandler { TaskTitle: CheckTaskschedDefaultTaskTitle, Folder: CheckTaskschedDefaultFolder, Recursive: CheckTaskschedDefaultRecursive, + Hidden: CheckTaskschedDefaultHidden, } } @@ -43,6 +46,7 @@ func (l *CheckTasksched) Build() *CheckData { "title": {value: &l.TaskTitle, description: "Sets the task to check. This corresonds to the title of the scheduled task, called TaskName in Powershell output."}, "folder": {value: &l.Folder, description: "The folder where the scheduled task is saved. This is used for exact matches, unless recurisive option is enabled."}, "recursive": {value: &l.Recursive, description: "Include the subfolders of the specified folder as well when searching for scheduled tasks."}, + "hidden": {value: &l.Hidden, description: "Include hidden tasks."}, }, defaultFilter: "enabled = true", defaultCritical: "exit_code < 0", @@ -73,7 +77,7 @@ func (l *CheckTasksched) Build() *CheckData { {name: "next_run_time", description: "Time when the registered task is next scheduled to run", unit: UDate}, {name: "parameters", description: "Last actions command line parameters"}, {name: "execute", description: "Last actions executed program"}, - {name: "working_dir", description: "Last actions working directory"}, + {name: "working_directory", description: "Last actions working directory"}, }, exampleDefault: ` check_tasksched diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 6029d68d..70cc8fec 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -29,6 +29,14 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD } } + // Remove backslash at the end of the folder path, if it is not exactly root: "\" + // "\Microsoft\" -> "\Microsoft" + if l.Folder != CheckTaskschedDefaultFolder && l.Folder != "\\" { + if cut, cutOk := strings.CutSuffix(l.Folder, "\\"); cutOk { + l.Folder = cut + } + } + titleRuneBlacklist := []rune{'\\', '/', ':', '*', '?', '"', '<', '>', '|'} if l.TaskTitle != CheckTaskschedDefaultTaskTitle { if strings.ContainsFunc(l.TaskTitle, func(r rune) bool { return slices.Contains(titleRuneBlacklist, r) }) { @@ -70,6 +78,14 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD specifyValue: true, specifiedValue: strconv.FormatBool(l.Recursive), }, + PowerShellParameter{ + name: "hidden", + parameterType: "string", + specifyDefaultValue: true, + defaultValue: strconv.FormatBool(CheckTaskschedDefaultHidden), + specifyValue: true, + specifiedValue: strconv.FormatBool(l.Hidden), + }, ) if err != nil { return fmt.Errorf("error when building a powershell command: %s", err.Error()) @@ -118,7 +134,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD "next_run_time": fmt.Sprintf("%d", l.parseDate(task.NextRunTime).Unix()), "parameters": l.parseParameters(task.Actions), "execute": l.parseExecuteCmd(task.Actions), - "working_dir": l.parseWorkingDir(task.Actions), + "working_directory": l.parseWorkingDir(task.Actions), } check.listData = append(check.listData, entry) } diff --git a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 index 59d4ee16..81dede8c 100644 --- a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 +++ b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 @@ -24,6 +24,11 @@ if ($args) { $i++ continue } + if ($args[$i] -eq '-hidden' -and $i + 1 -lt $args.Count) { + $hidden = $args[$i + 1] + $i++ + continue + } } } @@ -31,6 +36,13 @@ if ($args) { if (!$title) { $title = '*' } if (!$folder) { $folder = '\' } if (!$recursive) { $recursive = 'true' } +if (!$hidden) { $hidden = 'true' } + +# debug the parameters/arguments +[Console]::Error.WriteLine(('title: ' + $title )) +[Console]::Error.WriteLine(('folder: ' + $folder )) +[Console]::Error.WriteLine(('recursive: ' + $recursive )) +[Console]::Error.WriteLine(('hidden: ' + $hidden )) # ensure output is utf8 $OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 @@ -54,7 +66,12 @@ try { $currentFolder = $folderQueue.Dequeue() # TASK_ENUM_HIDDEN = 1, include hidden tasks # Call GetTasks() using TASK_ENUM_HIDDEN - foreach ($t in $currentFolder.GetTasks(1)) { + if ($hidden -eq 'true'){ + $getTasksArg = 1 + } else { + $getTasksArg = 0 + } + foreach ($t in $currentFolder.GetTasks($getTasksArg)) { $tasks.Add($t) } if ($recursive -eq 'true') { From 27fab8778486ce460006f2c86409d82adbce9fa9 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 23 Jun 2026 13:16:01 +0200 Subject: [PATCH 2/8] add UBool unit type to attributes, and converter for common boolean stting specifications the converter looks for common booleanish strings like 'yes' , 'off' 'disabled' 'True' etc, and converts them to 'true' or 'false' . these are then string-compared to the values the check returns for these attributes. use the Ubool attribute type on check_tasksched attributes: enabled, has_run and hidden. it is not used anywhere else --- pkg/snclient/check_tasksched.go | 6 ++-- pkg/snclient/checkdata.go | 1 + pkg/snclient/condition.go | 23 ++++++++++++- pkg/utils/utils.go | 59 +++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index 121b7247..f4fc92da 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -60,18 +60,18 @@ func (l *CheckTasksched) Build() *CheckData { {name: "application", description: "Name of the application that the task is associated with"}, {name: "comment", description: "Comment or description for the work item"}, {name: "creator", description: "Creator of the work item"}, - {name: "enabled", description: "Flag whether this job is enabled (true/false)"}, + {name: "enabled", description: "Flag whether this job is enabled (true/false)", unit: UBool}, {name: "exit_code", description: "The last jobs exit code"}, {name: "exit_string", description: "The last jobs exit code as string"}, {name: "folder", description: "Task folder"}, {name: "uri", description: "Fully qualified path to the task, includes folder and the task title"}, {name: "uri_clean", description: "Remove the leading backslash from the URI, only for tasks directly saved at root and not for ones saved inside folders."}, - {name: "has_run", description: "True if this task has ever been executed"}, + {name: "has_run", description: "True if this task has ever been executed", unit: UBool}, {name: "max_run_time", description: "Maximum length of time the task can run", unit: UDuration}, {name: "most_recent_run_time", description: "Most recent time the work item began running", unit: UDate}, {name: "priority", description: "Task priority"}, {name: "title", description: "Task title"}, - {name: "hidden", description: "Indicates that the task will not be visible in the UI (true/false)"}, + {name: "hidden", description: "Indicates that the task will not be visible in the UI (true/false)", unit: UBool}, {name: "missed_runs", description: "Number of times the registered task has missed a scheduled run"}, {name: "task_status", description: "Task status as string"}, {name: "next_run_time", description: "Time when the registered task is next scheduled to run", unit: UDate}, diff --git a/pkg/snclient/checkdata.go b/pkg/snclient/checkdata.go index 0103c14d..2948ed35 100644 --- a/pkg/snclient/checkdata.go +++ b/pkg/snclient/checkdata.go @@ -82,6 +82,7 @@ const ( UDate UTimestamp UPercent + UBool ) type CheckAttribute struct { diff --git a/pkg/snclient/condition.go b/pkg/snclient/condition.go index ec489c7e..b5b08176 100644 --- a/pkg/snclient/condition.go +++ b/pkg/snclient/condition.go @@ -16,6 +16,10 @@ import ( ) var ( + // ^ and $ are used to capture the whole string + // (\-?\d+\.\d+|\-?\d+) -> capture group one, two options separated by | first is a decimal with dot , second is a direct number + // \s* -> any number of whitespace between the second group + // (\D+) -> capture group two, one or more non-digit characters reConditionValueUnit = regexp.MustCompile(`^(\-?\d+\.\d+|\-?\d+)\s*(\D+)$`) reCuddleKeyword = regexp.MustCompile(`^([A-Za-z_]+)([!=><~]+)(.*)$`) reCuddleOperator = regexp.MustCompile(`^([!=><~]+)(.*?)$`) @@ -819,10 +823,25 @@ func (c *Condition) expandDateKeyword(str string) bool { func (c *Condition) expandUnitByType(str string) error { // valid units might be "today", "thisweek", "thismonth", "thisyear" and ":utc" variants unit := c.getUnit(c.keyword) - if unit == UDate || unit == UTimestamp { + + switch unit { + case UDate, UTimestamp: if done := c.expandDateKeyword(str); done { return nil } + case UBool: + // boolean units are not in the form of ' ' + // need to handle them before regex condition checks. + newVal, oldVal, err := utils.ParseAndReplaceBoolAttributes(str) + if err != nil { + return fmt.Errorf("invalid boolean value: %s", err.Error()) + } + c.original = oldVal + c.value = newVal + c.unit = "" + + return nil + default: } match := reConditionValueUnit.FindStringSubmatch(str) @@ -868,6 +887,8 @@ func (c *Condition) expandUnitByType(str string) error { c.unit = "s" return nil + case UBool: + return nil // handled before the regex check case UPercent: return nil case UNone: diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 78713a8e..9312c2c6 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -81,6 +81,65 @@ func ExpandDuration(val string) (res float64, err error) { return 0, fmt.Errorf("expandDuration: cannot parse duration, unknown format in %s", val) } +var truthyValues = []string{"1", "true", "enabled", "yes", "y", "on", "t"} +var falseyValues = []string{"0", "false", "disabled", "no", "n", "off", "f"} + +var ( + truthyRegex, truthyReplacements = replacePatterns(truthyValues, "true") + falseyRegex, falseyReplacements = replacePatterns(falseyValues, "false") +) + +// replacePatterns builds a regex that matches any of the given words as whole words, +// and returns the pattern and a replacement map (lowercase match → replacement). +func replacePatterns(words []string, replacement string) (regex *regexp.Regexp, replacements map[string]string) { + escaped := make([]string, len(words)) + replacementMap := make(map[string]string) + for i, word := range words { + lowercase := strings.ToLower(word) + escaped[i] = regexp.QuoteMeta(lowercase) + replacementMap[lowercase] = replacement + } + pattern := `\b(` + strings.Join(escaped, "|") + `)\b` + + return regexp.MustCompile(pattern), replacementMap +} + +// ParseAndReplaceBoolAttributes replaces all known truthy/falsey tokens in val +// with "true"/"false" (case‑insensitive whole‑word match). +// Returns the modified string, the original string, and an error if any +// unrecognized boolean‑like token is found (optional). +func ParseAndReplaceBoolAttributes(val string) (newVal, oldVal string, err error) { + oldVal = val + foundValues := 0 + + newVal = truthyRegex.ReplaceAllStringFunc(val, func(match string) string { + if replacement, ok := truthyReplacements[strings.ToLower(match)]; ok { + foundValues++ + + return replacement + } + + return "" + }) + + newVal = falseyRegex.ReplaceAllStringFunc(newVal, func(match string) string { + if replacement, ok := falseyReplacements[strings.ToLower(match)]; ok { + foundValues++ + + return replacement + } + + return "" + }) + + if foundValues == 0 { + err = fmt.Errorf("ParseAndReplaceBoolAttributes: did not find any truthy or falsely values in boolean attribute value: '%s' , lowercase needs to be one of falsely: '%s' or truthy: '%s' values", + val, strings.Join(falseyValues, " "), strings.Join(truthyValues, " ")) + } + + return newVal, oldVal, err +} + // returns time/duration in target unit with given precision func TimeUnitF(num uint64, targetUnit string, precision int) float64 { for _, factor := range TimeFactors { From cceafcb6f29f1c226f8229abd1b837880012b9e0 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 23 Jun 2026 13:35:02 +0200 Subject: [PATCH 3/8] check_tasksched: add default values of arguments in their descriptions hidden arg is default false in the script as well fix some other typos/dead code --- pkg/snclient/check_tasksched.go | 15 ++++++++++----- pkg/snclient/condition.go | 3 --- .../embed/scripts/windows/scheduled_tasks.ps1 | 2 +- pkg/utils/utils.go | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index f4fc92da..232e0622 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -42,11 +42,16 @@ func (l *CheckTasksched) Build() *CheckData { State: CheckExitOK, }, args: map[string]CheckArgument{ - "timezone": {description: "Sets the timezone for time metrics (default is local time)"}, - "title": {value: &l.TaskTitle, description: "Sets the task to check. This corresonds to the title of the scheduled task, called TaskName in Powershell output."}, - "folder": {value: &l.Folder, description: "The folder where the scheduled task is saved. This is used for exact matches, unless recurisive option is enabled."}, - "recursive": {value: &l.Recursive, description: "Include the subfolders of the specified folder as well when searching for scheduled tasks."}, - "hidden": {value: &l.Hidden, description: "Include hidden tasks."}, + "timezone": {description: "Sets the timezone for time metrics (default is local time)"}, + "title": {value: &l.TaskTitle, + description: fmt.Sprintf("Sets the task to check. This corresonds to the title of the scheduled task. Default: '%s'", CheckTaskschedDefaultTaskTitle)}, + "folder": {value: &l.Folder, + description: fmt.Sprintf("The folder where the scheduled task is saved. This is used for exact matches, unless recurisive option is enabled. Default: '%s'", + CheckTaskschedDefaultFolder)}, + "recursive": {value: &l.Recursive, + description: fmt.Sprintf("Include the subfolders of the specified folder as well when searching for scheduled tasks. Default: '%t'", CheckTaskschedDefaultRecursive)}, + "hidden": {value: &l.Hidden, + description: fmt.Sprintf("Include hidden tasks. Default: '%t'", CheckTaskschedDefaultHidden)}, }, defaultFilter: "enabled = true", defaultCritical: "exit_code < 0", diff --git a/pkg/snclient/condition.go b/pkg/snclient/condition.go index b5b08176..5481e7c8 100644 --- a/pkg/snclient/condition.go +++ b/pkg/snclient/condition.go @@ -841,7 +841,6 @@ func (c *Condition) expandUnitByType(str string) error { c.unit = "" return nil - default: } match := reConditionValueUnit.FindStringSubmatch(str) @@ -887,8 +886,6 @@ func (c *Condition) expandUnitByType(str string) error { c.unit = "s" return nil - case UBool: - return nil // handled before the regex check case UPercent: return nil case UNone: diff --git a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 index 81dede8c..c063cc9e 100644 --- a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 +++ b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 @@ -36,7 +36,7 @@ if ($args) { if (!$title) { $title = '*' } if (!$folder) { $folder = '\' } if (!$recursive) { $recursive = 'true' } -if (!$hidden) { $hidden = 'true' } +if (!$hidden) { $hidden = 'false' } # debug the parameters/arguments [Console]::Error.WriteLine(('title: ' + $title )) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 9312c2c6..aa3d3573 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -133,7 +133,7 @@ func ParseAndReplaceBoolAttributes(val string) (newVal, oldVal string, err error }) if foundValues == 0 { - err = fmt.Errorf("ParseAndReplaceBoolAttributes: did not find any truthy or falsely values in boolean attribute value: '%s' , lowercase needs to be one of falsely: '%s' or truthy: '%s' values", + err = fmt.Errorf("ParseAndReplaceBoolAttributes: did not find any truthy or falsey values in boolean attribute value: '%s' , lowercase needs to be one of falsey: '%s' or truthy: '%s' values", val, strings.Join(falseyValues, " "), strings.Join(truthyValues, " ")) } From f45ce7c40301142284d24cccc84ff4d2046d706d Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Tue, 23 Jun 2026 13:50:29 +0200 Subject: [PATCH 4/8] check_tasksched: citest fixes update docs split up the addTasks function since it was getting too long, and had logical parts to be split others are spacing, linting fixes from golangci-lint --- docs/checks/commands/check_tasksched.md | 9 +- pkg/snclient/check_tasksched.go | 24 +++-- pkg/snclient/check_tasksched_windows.go | 112 +++++++++++++----------- pkg/snclient/condition.go | 4 + pkg/utils/utils.go | 6 +- 5 files changed, 89 insertions(+), 66 deletions(-) diff --git a/docs/checks/commands/check_tasksched.md b/docs/checks/commands/check_tasksched.md index 2594624d..dcf6cd5c 100644 --- a/docs/checks/commands/check_tasksched.md +++ b/docs/checks/commands/check_tasksched.md @@ -56,10 +56,11 @@ Naemon Config | Argument | Description | | --------- | --------------------------------------------------------------------------------------------------------- | -| folder | The folder where the scheduled task is saved. This is used for exact matches, unless recurisive option is enabled. | -| recursive | Include the subfolders of the specified folder as well when searching for scheduled tasks. | +| folder | The folder where the scheduled task is saved. This is used for exact matches, unless recurisive option is enabled. Default: '\' | +| hidden | Include hidden tasks. Default: 'false' | +| recursive | Include the subfolders of the specified folder as well when searching for scheduled tasks. Default: 'true' | | timezone | Sets the timezone for time metrics (default is local time) | -| title | Sets the task to check. This corresonds to the title of the scheduled task, called TaskName in Powershell output. | +| title | Sets the task to check. This corresonds to the title of the scheduled task. Default: '\*' | ## Attributes @@ -89,4 +90,4 @@ these can be used in filters and thresholds (along with the default attributes): | next_run_time | Time when the registered task is next scheduled to run | | parameters | Last actions command line parameters | | execute | Last actions executed program | -| working_dir | Last actions working directory | +| working_directory | Last actions working directory | diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index 232e0622..da0c2f07 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -43,15 +43,23 @@ func (l *CheckTasksched) Build() *CheckData { }, args: map[string]CheckArgument{ "timezone": {description: "Sets the timezone for time metrics (default is local time)"}, - "title": {value: &l.TaskTitle, - description: fmt.Sprintf("Sets the task to check. This corresonds to the title of the scheduled task. Default: '%s'", CheckTaskschedDefaultTaskTitle)}, - "folder": {value: &l.Folder, + "title": { + value: &l.TaskTitle, + description: fmt.Sprintf("Sets the task to check. This corresonds to the title of the scheduled task. Default: '%s'", CheckTaskschedDefaultTaskTitle), + }, + "folder": { + value: &l.Folder, description: fmt.Sprintf("The folder where the scheduled task is saved. This is used for exact matches, unless recurisive option is enabled. Default: '%s'", - CheckTaskschedDefaultFolder)}, - "recursive": {value: &l.Recursive, - description: fmt.Sprintf("Include the subfolders of the specified folder as well when searching for scheduled tasks. Default: '%t'", CheckTaskschedDefaultRecursive)}, - "hidden": {value: &l.Hidden, - description: fmt.Sprintf("Include hidden tasks. Default: '%t'", CheckTaskschedDefaultHidden)}, + CheckTaskschedDefaultFolder), + }, + "recursive": { + value: &l.Recursive, + description: fmt.Sprintf("Include the subfolders of the specified folder as well when searching for scheduled tasks. Default: '%t'", CheckTaskschedDefaultRecursive), + }, + "hidden": { + value: &l.Hidden, + description: fmt.Sprintf("Include hidden tasks. Default: '%t'", CheckTaskschedDefaultHidden), + }, }, defaultFilter: "enabled = true", defaultCritical: "exit_code < 0", diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 70cc8fec..a5b39d0b 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -6,6 +6,7 @@ import ( "context" _ "embed" "fmt" + "os/exec" "slices" "strconv" "strings" @@ -18,24 +19,8 @@ import ( //go:embed embed/scripts/windows/scheduled_tasks.ps1 var scheduledTasksPS1 string -//nolint:funlen // function is long, but is simple, should not be dismantled func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error { - script := scheduledTasksPS1 - - // Add backslash to the beginning of the folder path if it does not exist - if l.Folder != CheckTaskschedDefaultFolder { - if !strings.HasPrefix(l.Folder, "\\") { - l.Folder = "\\" + l.Folder - } - } - - // Remove backslash at the end of the folder path, if it is not exactly root: "\" - // "\Microsoft\" -> "\Microsoft" - if l.Folder != CheckTaskschedDefaultFolder && l.Folder != "\\" { - if cut, cutOk := strings.CutSuffix(l.Folder, "\\"); cutOk { - l.Folder = cut - } - } + l.cleanupArguments() titleRuneBlacklist := []rune{'\\', '/', ':', '*', '?', '"', '<', '>', '|'} if l.TaskTitle != CheckTaskschedDefaultTaskTitle { @@ -52,41 +37,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD } } - cmd, err := powerShellCmd( - ctx, script, - PowerShellParameter{ - name: "title", - parameterType: "string", - specifyDefaultValue: true, - defaultValue: CheckTaskschedDefaultTaskTitle, - specifyValue: true, - specifiedValue: l.TaskTitle, - }, - PowerShellParameter{ - name: "folder", - parameterType: "string", - specifyDefaultValue: true, - defaultValue: CheckTaskschedDefaultFolder, - specifyValue: true, - specifiedValue: l.Folder, - }, - PowerShellParameter{ - name: "recursive", - parameterType: "string", - specifyDefaultValue: true, - defaultValue: strconv.FormatBool(CheckTaskschedDefaultRecursive), - specifyValue: true, - specifiedValue: strconv.FormatBool(l.Recursive), - }, - PowerShellParameter{ - name: "hidden", - parameterType: "string", - specifyDefaultValue: true, - defaultValue: strconv.FormatBool(CheckTaskschedDefaultHidden), - specifyValue: true, - specifiedValue: strconv.FormatBool(l.Hidden), - }, - ) + cmd, err := l.buildPowershellCmd(ctx) if err != nil { return fmt.Errorf("error when building a powershell command: %s", err.Error()) } @@ -147,6 +98,63 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD return nil } +func (l *CheckTasksched) cleanupArguments() { + // Add backslash to the beginning of the folder path if it does not exist + if l.Folder != CheckTaskschedDefaultFolder { + if !strings.HasPrefix(l.Folder, "\\") { + l.Folder = "\\" + l.Folder + } + } + + // Remove backslash at the end of the folder path, if it is not exactly root: "\" + // "\Microsoft\" -> "\Microsoft" + if l.Folder != CheckTaskschedDefaultFolder && l.Folder != "\\" { + if cut, cutOk := strings.CutSuffix(l.Folder, "\\"); cutOk { + l.Folder = cut + } + } +} + +func (l *CheckTasksched) buildPowershellCmd(ctx context.Context) (cmd *exec.Cmd, err error) { + cmd, err = powerShellCmd( + ctx, scheduledTasksPS1, + PowerShellParameter{ + name: "title", + parameterType: "string", + specifyDefaultValue: true, + defaultValue: CheckTaskschedDefaultTaskTitle, + specifyValue: true, + specifiedValue: l.TaskTitle, + }, + PowerShellParameter{ + name: "folder", + parameterType: "string", + specifyDefaultValue: true, + defaultValue: CheckTaskschedDefaultFolder, + specifyValue: true, + specifiedValue: l.Folder, + }, + PowerShellParameter{ + name: "recursive", + parameterType: "string", + specifyDefaultValue: true, + defaultValue: strconv.FormatBool(CheckTaskschedDefaultRecursive), + specifyValue: true, + specifiedValue: strconv.FormatBool(l.Recursive), + }, + PowerShellParameter{ + name: "hidden", + parameterType: "string", + specifyDefaultValue: true, + defaultValue: strconv.FormatBool(CheckTaskschedDefaultHidden), + specifyValue: true, + specifiedValue: strconv.FormatBool(l.Hidden), + }, + ) + + return cmd, err +} + func parseURIClean(uri string) string { if strings.Count(uri, "\\") == 1 { if cut, cutOk := strings.CutPrefix(uri, "\\"); cutOk { diff --git a/pkg/snclient/condition.go b/pkg/snclient/condition.go index 5481e7c8..c56dbb85 100644 --- a/pkg/snclient/condition.go +++ b/pkg/snclient/condition.go @@ -820,6 +820,7 @@ func (c *Condition) expandDateKeyword(str string) bool { return false } +//nolint:funlen // the function is long due to handling all unit types, but it is simple func (c *Condition) expandUnitByType(str string) error { // valid units might be "today", "thisweek", "thismonth", "thisyear" and ":utc" variants unit := c.getUnit(c.keyword) @@ -841,6 +842,7 @@ func (c *Condition) expandUnitByType(str string) error { c.unit = "" return nil + default: } match := reConditionValueUnit.FindStringSubmatch(str) @@ -888,6 +890,8 @@ func (c *Condition) expandUnitByType(str string) error { return nil case UPercent: return nil + case UBool: + return nil // handled in the switch above case UNone: // best effort unit expansion return c.expandUnitByName(str) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index aa3d3573..8867a162 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -81,8 +81,10 @@ func ExpandDuration(val string) (res float64, err error) { return 0, fmt.Errorf("expandDuration: cannot parse duration, unknown format in %s", val) } -var truthyValues = []string{"1", "true", "enabled", "yes", "y", "on", "t"} -var falseyValues = []string{"0", "false", "disabled", "no", "n", "off", "f"} +var ( + truthyValues = []string{"1", "true", "enabled", "yes", "y", "on", "t"} + falseyValues = []string{"0", "false", "disabled", "no", "n", "off", "f"} +) var ( truthyRegex, truthyReplacements = replacePatterns(truthyValues, "true") From dfa45a04ebdb084de52793cfc939557898979899 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 23 Jun 2026 14:05:37 +0200 Subject: [PATCH 5/8] remove time measurements and debug stderr printouts in check_taskscheduled embedded script --- .../embed/scripts/windows/scheduled_tasks.ps1 | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 index c063cc9e..aa675486 100644 --- a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 +++ b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 @@ -38,25 +38,12 @@ if (!$folder) { $folder = '\' } if (!$recursive) { $recursive = 'true' } if (!$hidden) { $hidden = 'false' } -# debug the parameters/arguments -[Console]::Error.WriteLine(('title: ' + $title )) -[Console]::Error.WriteLine(('folder: ' + $folder )) -[Console]::Error.WriteLine(('recursive: ' + $recursive )) -[Console]::Error.WriteLine(('hidden: ' + $hidden )) - # ensure output is utf8 $OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 -# Print powershell version -[Console]::Error.WriteLine(('Powershell version table: ' + ($PSVersionTable | ConvertTo-Json -Compress))) - -$sw = [System.Diagnostics.Stopwatch]::StartNew() $scheduler = New-Object -ComObject Schedule.Service $scheduler.Connect() -$sw.Stop() -[Console]::Error.WriteLine(('COM Schedule.Service connect took {0:F2} ms' -f $sw.Elapsed.TotalMilliseconds)) -$sw = [System.Diagnostics.Stopwatch]::StartNew() $tasks = [System.Collections.Generic.List[object]]::new() try { $targetFolder = $scheduler.GetFolder($folder) @@ -83,8 +70,6 @@ try { } catch { $tasks = [System.Collections.Generic.List[object]]::new() } -$sw.Stop() -[Console]::Error.WriteLine(('Task enumeration took {0:F2} ms' -f $sw.Elapsed.TotalMilliseconds)) if ($title -ne '*') { $filtered = [System.Collections.Generic.List[object]]::new() @@ -96,7 +81,6 @@ if ($title -ne '*') { $tasks = $filtered } -$sw = [System.Diagnostics.Stopwatch]::StartNew() $results = [System.Collections.Generic.List[object]]::new() foreach ($task in $tasks) { $def = $task.Definition @@ -140,17 +124,11 @@ foreach ($task in $tasks) { } ) } -$sw.Stop() -[Console]::Error.WriteLine(('Populating results list took {0:F2} ms' -f $sw.Elapsed.TotalMilliseconds)) -[Console]::Error.WriteLine(('Results list has {0} elements' -f $results.Count)) -$sw = [System.Diagnostics.Stopwatch]::StartNew() if ($results.Count -gt 0) { ConvertTo-Json -InputObject $results -Depth 4 } else { '[]' } -$sw.Stop() -[Console]::Error.WriteLine(('Converting to JSON took {0:F2} ms' -f $sw.Elapsed.TotalMilliseconds)) exit 0 \ No newline at end of file From 167b921bfff3aa405fa3730a84081f2b27bb1883 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 23 Jun 2026 14:07:03 +0200 Subject: [PATCH 6/8] fix has_run attribute, was only checking if last run time isnt empty in reality, last run time is always not empty and has some default value --- pkg/snclient/check_tasksched_windows.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index a5b39d0b..a3e200bd 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -19,6 +19,11 @@ import ( //go:embed embed/scripts/windows/scheduled_tasks.ps1 var scheduledTasksPS1 string +// if the task is not run, this date is reported +// corresponds to 1999-11-30 00:00:00 CET +// the number is unix miliseconds +const notRunDate = "/Date(943916400000)/" + func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error { l.cleanupArguments() @@ -60,7 +65,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD for index := range taskList { task := taskList[index] hasRun := false - if task.LastRunTime != "" { + if task.LastRunTime != notRunDate { hasRun = true } From fcef914c862057a0e41d66824a711d4d71de7119 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 23 Jun 2026 14:59:49 +0200 Subject: [PATCH 7/8] fix replacePatterns not using case-insensitive regex --- pkg/utils/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 8867a162..06b201c8 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -101,7 +101,7 @@ func replacePatterns(words []string, replacement string) (regex *regexp.Regexp, escaped[i] = regexp.QuoteMeta(lowercase) replacementMap[lowercase] = replacement } - pattern := `\b(` + strings.Join(escaped, "|") + `)\b` + pattern := `(?i)\b(` + strings.Join(escaped, "|") + `)\b` return regexp.MustCompile(pattern), replacementMap } From 6ae03f6ec00e7b80f20d87a491b20af9a04db0fe Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 23 Jun 2026 15:22:59 +0200 Subject: [PATCH 8/8] decide on a common error message for invalid boolean attribute parsing --- pkg/utils/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 06b201c8..c0a251e5 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -135,7 +135,7 @@ func ParseAndReplaceBoolAttributes(val string) (newVal, oldVal string, err error }) if foundValues == 0 { - err = fmt.Errorf("ParseAndReplaceBoolAttributes: did not find any truthy or falsey values in boolean attribute value: '%s' , lowercase needs to be one of falsey: '%s' or truthy: '%s' values", + err = fmt.Errorf("attribute of type bool can not be parsed: '%s' needs to be one of '%s' or '%s'", val, strings.Join(falseyValues, " "), strings.Join(truthyValues, " ")) }