From 862b2f77853c86eff9ae60e4a154b072d4428f34 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Fri, 12 Jun 2026 19:04:15 +0200 Subject: [PATCH 01/14] check_tasksched: add argument title by default it is set as '*', but that is cosmetic. If it set to anything else, the embedded powershell script is called with title=[title] parameter passing that title needs some weird workarounds due to way snclient runs powershell scripts the powershell script is however normal, it just then looks for tasks that contain [title] as substring if title parameter is present it uses Get-ScheduledTask -TaskName ('*' + $title + '*') cmdlet instead of bare Get-ScheduledTask which would iterate over all tasks this speeds up the check if a task name is known. I named it title, as it was being added to the entry as "title", can change that if needed --- pkg/snclient/check_tasksched.go | 13 +++++++++++-- pkg/snclient/check_tasksched_windows.go | 7 ++++++- .../embed/scripts/windows/scheduled_tasks.ps1 | 12 ++++++++++-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index a3e23ef5..6e905052 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -10,10 +10,18 @@ func init() { AvailableChecks["check_tasksched"] = CheckEntry{"check_tasksched", NewCheckTasksched} } -type CheckTasksched struct{} +type CheckTasksched struct { + TaskTitle string +} + +const ( + CheckTaskschedDefaultTaskTitle string = "*" +) func NewCheckTasksched() CheckHandler { - return &CheckTasksched{} + return &CheckTasksched{ + TaskTitle: CheckTaskschedDefaultTaskTitle, + } } func (l *CheckTasksched) Build() *CheckData { @@ -26,6 +34,7 @@ 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: "Sets the task to check. This corresonds to the title of the task, used when iteratating over tasks."}, }, defaultFilter: "enabled = true", defaultCritical: "exit_code < 0", diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 3f22e4e1..0c5cf333 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -7,6 +7,7 @@ import ( _ "embed" "fmt" "strconv" + "strings" "syscall" "time" @@ -17,7 +18,11 @@ import ( var scheduledTasksPS1 string func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error { - cmd := powerShellCmd(ctx, scheduledTasksPS1) + script := scheduledTasksPS1 + if l.TaskTitle != CheckTaskschedDefaultTaskTitle { + script = "$title = '" + strings.ReplaceAll(l.TaskTitle, "'", "''") + "'; " + script + } + cmd := powerShellCmd(ctx, script) output, stderr, exitCode, _, err := snc.runExternalCommand(ctx, cmd, snc.getBuiltinCmdTimeout()) if err != nil { return fmt.Errorf("getting scheduled tasks failed: %s\n%s", err.Error(), stderr) diff --git a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 index 61c3b2d2..2d4f35e8 100644 --- a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 +++ b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 @@ -1,11 +1,19 @@ # list scheduled tasks in json format -# usage: .\scheduled_tasks.ps1 +# usage: .\scheduled_tasks.ps1 [-title ] # +if (!$title) { $title = $null } + # ensure output is utf8 $OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 -Get-ScheduledTask | ForEach-Object { +if ($title) { + $tasks = Get-ScheduledTask -TaskName ('*' + $title + '*') +} else { + $tasks = Get-ScheduledTask +} + +$tasks | ForEach-Object { $task = $_ $taskInfo = Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath From 42d5f5955edf9e8e613b6ae9dc5cf78afca015f3 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Mon, 15 Jun 2026 10:46:15 +0200 Subject: [PATCH 02/14] add powerShellCmdAddVariableDefinition function instead of appending the variable definition to the script at the last point, and then calling powerShellCmd(ctx,script) , implement that functionality into powerShellCmdAddVariableDefinition this searches the point where the script definition starts, and then adds the variable definition. reusable and less confusing about what it does. --- pkg/snclient/check_tasksched_windows.go | 8 ++++--- pkg/snclient/snclient_windows.go | 29 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 0c5cf333..bb55ac88 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -7,7 +7,6 @@ import ( _ "embed" "fmt" "strconv" - "strings" "syscall" "time" @@ -19,10 +18,13 @@ var scheduledTasksPS1 string func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error { script := scheduledTasksPS1 + cmd := powerShellCmd(ctx, script) if l.TaskTitle != CheckTaskschedDefaultTaskTitle { - script = "$title = '" + strings.ReplaceAll(l.TaskTitle, "'", "''") + "'; " + script + err := powerShellCmdAddVariableDefinition(cmd, "title", l.TaskTitle) + if err != nil { + return err + } } - cmd := powerShellCmd(ctx, script) output, stderr, exitCode, _, err := snc.runExternalCommand(ctx, cmd, snc.getBuiltinCmdTimeout()) if err != nil { return fmt.Errorf("getting scheduled tasks failed: %s\n%s", err.Error(), stderr) diff --git a/pkg/snclient/snclient_windows.go b/pkg/snclient/snclient_windows.go index 1819a7f5..74bc658a 100644 --- a/pkg/snclient/snclient_windows.go +++ b/pkg/snclient/snclient_windows.go @@ -396,14 +396,41 @@ func shell() string { func powerShellCmd(ctx context.Context, command string) (cmd *exec.Cmd) { cmd = exec.CommandContext(ctx, "powershell") cmd.Args = nil + cmdLine := fmt.Sprintf(`%s -Command "%s"`, POWERSHELL, command) //nolint:gocritic // using %q just breaks the command from escaping newlines cmd.SysProcAttr = &syscall.SysProcAttr{ HideWindow: true, - CmdLine: fmt.Sprintf(`%s -Command "%s"`, POWERSHELL, command), //nolint:gocritic // using %q just breaks the command from escaping newlines + CmdLine: cmdLine, } return cmd } +// this function finds where the powershell command body starts, and prepends a variable definition with a value. +func powerShellCmdAddVariableDefinition(cmd *exec.Cmd, variableName, variableValue string) (err error) { + if cmd == nil { + return errors.New("powershell command that was passed is nil") + } + if cmd.SysProcAttr.CmdLine == "" { + return errors.New("powershell command has empty SysProcAttr.CmdLine. This function can only modify it when its present") + } + + cmdline := cmd.SysProcAttr.CmdLine + cmdlinePrefix := fmt.Sprintf(`%s -Command "`, POWERSHELL) + + if cut, cutOk := strings.CutPrefix(cmdline, cmdlinePrefix); cutOk { + newCmdline := cmdlinePrefix + + fmt.Sprintf(`$%s = '%s';`, variableName, strings.ReplaceAll(variableValue, "'", "''")) + + "\r\n" + + cut + + cmd.SysProcAttr.CmdLine = newCmdline + + return nil + } else { + return errors.New("powershell command does not start with the expected prefix") + } +} + func isBatchFile(path string) bool { ext := filepath.Ext(path) From fd50967f19fc1948b566e7e56f077bae3974323c Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Mon, 15 Jun 2026 11:25:26 +0200 Subject: [PATCH 03/14] check_tasksched: citest fixes --- docs/checks/commands/check_tasksched.md | 7 ++++--- pkg/snclient/snclient_windows.go | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/checks/commands/check_tasksched.md b/docs/checks/commands/check_tasksched.md index ed98ac16..0309f4eb 100644 --- a/docs/checks/commands/check_tasksched.md +++ b/docs/checks/commands/check_tasksched.md @@ -54,9 +54,10 @@ Naemon Config ## Check Specific Arguments -| Argument | Description | -| -------- | ---------------------------------------------------------- | -| timezone | Sets the timezone for time metrics (default is local time) | +| Argument | Description | +| -------- | --------------------------------------------------------------------------------------------------- | +| timezone | Sets the timezone for time metrics (default is local time) | +| title | Sets the task to check. This corresonds to the title of the task, used when iteratating over tasks. | ## Attributes diff --git a/pkg/snclient/snclient_windows.go b/pkg/snclient/snclient_windows.go index 74bc658a..2ef86c35 100644 --- a/pkg/snclient/snclient_windows.go +++ b/pkg/snclient/snclient_windows.go @@ -426,9 +426,9 @@ func powerShellCmdAddVariableDefinition(cmd *exec.Cmd, variableName, variableVal cmd.SysProcAttr.CmdLine = newCmdline return nil - } else { - return errors.New("powershell command does not start with the expected prefix") } + + return errors.New("powershell command does not start with the expected prefix") } func isBatchFile(path string) bool { From d5b1d68ca9cd840936ec5b8148c0d0d8a30d207f Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Mon, 15 Jun 2026 15:31:08 +0200 Subject: [PATCH 04/14] check_tasksched: add folder and recursive arguments the script now filters its task discovery step based on folder definitions, optionally with a wildcard at the end if recursive option is enabled the scirpt now does not exit with returncode != 0 if it cant find any matching tasks. the script now always outputs a valid json array, even if it finds no tasks, one task or multiple tasks. this prevents errors when parsing script output the script now reports Principal, Trigger, Setting, and Action nested objects of a TaskInfo. this is parsed more in detail on the go side, but not everything is added to the entry. --- pkg/snclient/check_tasksched.go | 10 +- pkg/snclient/check_tasksched_windows.go | 113 ++++++++++--- .../embed/scripts/windows/scheduled_tasks.ps1 | 153 +++++++++++++++--- 3 files changed, 223 insertions(+), 53 deletions(-) diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index 6e905052..140256a5 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -12,6 +12,8 @@ func init() { type CheckTasksched struct { TaskTitle string + Folder string + Rerursive bool } const ( @@ -21,6 +23,8 @@ const ( func NewCheckTasksched() CheckHandler { return &CheckTasksched{ TaskTitle: CheckTaskschedDefaultTaskTitle, + Folder: "\\", + Rerursive: false, } } @@ -33,8 +37,10 @@ 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 task, used when iteratating over tasks."}, + "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 task, used when iteratating over tasks."}, + "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.Rerursive, description: "Include the subfolders of the specified folder as well when searching for scheduled tasks."}, }, defaultFilter: "enabled = true", defaultCritical: "exit_code < 0", diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index bb55ac88..c54ca421 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -19,18 +19,25 @@ var scheduledTasksPS1 string func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error { script := scheduledTasksPS1 cmd := powerShellCmd(ctx, script) - if l.TaskTitle != CheckTaskschedDefaultTaskTitle { - err := powerShellCmdAddVariableDefinition(cmd, "title", l.TaskTitle) - if err != nil { - return err - } + err := powerShellCmdAddVariableDefinition(cmd, "title", l.TaskTitle) + if err != nil { + return err + } + err = powerShellCmdAddVariableDefinition(cmd, "folder", l.Folder) + if err != nil { + return err } + err = powerShellCmdAddVariableDefinition(cmd, "recursive", strconv.FormatBool(l.Rerursive)) + if err != nil { + return err + } + output, stderr, exitCode, _, err := snc.runExternalCommand(ctx, cmd, snc.getBuiltinCmdTimeout()) if err != nil { - return fmt.Errorf("getting scheduled tasks failed: %s\n%s", err.Error(), stderr) + return fmt.Errorf("getting scheduled tasks failed, error: %s\n%s", err.Error(), stderr) } if exitCode != 0 { - return fmt.Errorf("getting scheduled tasks failed: %s\n%s", output, stderr) + return fmt.Errorf("getting scheduled tasks failed, exitCode: %d\n%s", exitCode, stderr) } var taskList []ScheduledTask @@ -50,18 +57,18 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD entry := map[string]string{ "application": task.Name, "comment": task.Description, - "creator": task.Author, + "creator": task.UserID, "enabled": fmt.Sprintf("%t", task.Enabled), "exit_code": fmt.Sprintf("%d", task.LastTaskResult.exitCode()), "exit_string": task.LastTaskResult.String(), "folder": task.Path, "has_run": fmt.Sprintf("%t", hasRun), - "max_run_time": task.TimeLimit, + "max_run_time": task.ExecutionTimeLimit, "most_recent_run_time": fmt.Sprintf("%d", l.parseDate(task.LastRunTime).Unix()), "priority": fmt.Sprintf("%d", task.Priority), "title": task.Name, "hidden": fmt.Sprintf("%t", task.Hidden), - "missed_runs": fmt.Sprintf("%d", task.MissedRuns), + "missed_runs": fmt.Sprintf("%d", task.NumberOfMissedRuns), "task_status": task.State.String(), "next_run_time": fmt.Sprintf("%d", l.parseDate(task.NextRunTime).Unix()), "parameters": parameters, @@ -200,23 +207,77 @@ func (t TaskState) String() string { } type ScheduledTask struct { - Name string `json:"TaskName"` - Path string `json:"TaskPath"` - Description string `json:"Description"` - LastRunTime string `json:"LastRunTime"` - State TaskState `json:"State"` - NextRunTime string `json:"NextRunTime"` - LastTaskResult TaskResult `json:"LastTaskResult"` - MissedRuns int64 `json:"MissedRuns"` - Author string `json:"Author"` - Enabled bool `json:"Enabled"` - Priority int64 `json:"Priority"` - Hidden bool `json:"Hidden"` - TimeLimit string `json:"TimeLimit"` - Actions []ScheduledTaskAction `json:"Actions"` + Name string `json:"TaskName"` + Path string `json:"TaskPath"` + Description string `json:"Description"` + PSComputerName string `json:"PSComputerName"` + URI string `json:"URI"` + Version string `json:"Version"` + LastRunTime string `json:"LastRunTime"` + State TaskState `json:"State"` + NextRunTime string `json:"NextRunTime"` + LastTaskResult TaskResult `json:"LastTaskResult"` + NumberOfMissedRuns int64 `json:"NumberOfMissedRuns"` + UserID string `json:"UserId"` + Enabled bool `json:"Enabled"` + Priority int64 `json:"Priority"` + Hidden bool `json:"Hidden"` + ExecutionTimeLimit string `json:"ExecutionTimeLimit"` + Principal ScheduledTaskPrincipal `json:"Principal"` + Actions []ScheduledTaskAction `json:"Actions"` + Triggers []ScheduledTaskTrigger `json:"Triggers"` + Settings ScheduledTaskSetting `json:"Settings"` +} + +type ScheduledTaskPrincipal struct { + DisplayName string `json:"DisplayName"` + ID string `json:"Id"` + GroupID string `json:"GroupId"` + PSComputerName string `json:"PSComputerName"` + RequiredPrivilege []string `json:"RequiredPrivilege"` + UserID string `json:"UserId"` +} + +type ScheduledTaskTrigger struct { + DaysInterval int64 `json:"DaysInterval"` + Enabled bool `json:"Enabled"` + EndBoundary string `json:"EndBoundary"` + ExecutionTimeLimit string `json:"ExecutionTimeLimit"` + ID string `json:"Id"` + RandomDelay string `json:"RandomDelay"` + Repetition interface{} `json:"Repetition"` + StartBoundary string `json:"StartBoundary"` +} + +type ScheduledTaskSetting struct { + AllowDemandStart bool `json:"AllowDemandStart"` + AllowHardTerminate bool `json:"AllowHardTerminate"` + DeleteExpiredTaskAfter string `json:"DeleteExpiredTaskAfter"` + DisallowStartIfOnBatteries bool `json:"DisallowStartIfOnBatteries"` + DisallowStartOnRemoteAppSession bool `json:"DisallowStartOnRemoteAppSession"` + Enabled bool `json:"Enabled"` + ExecutionTimeLimit string `json:"ExecutionTimeLimit"` + Hidden bool `json:"Hidden"` + IdleSettings interface{} `json:"IdleSettings"` + MaintenanceSettings interface{} `json:"MaintenanceSettings"` + NetworkSettings interface{} `json:"NetworkSettings"` + Priority int64 `json:"Priority"` + PSComputerName string `json:"PSComputerName"` + RestartCount int64 `json:"RestartCount"` + RestartInterval string `json:"RestartInterval"` + RunOnlyIfIdle bool `json:"RunOnlyIfIdle"` + RunOnlyIfNetworkAvailable bool `json:"RunOnlyIfNetworkAvailable"` + StartWhenAvailable bool `json:"StartWhenAvailable"` + StopIfGoingOnBatteries bool `json:"StopIfGoingOnBatteries"` + UseUnifiedSchedulingEngine bool `json:"UseUnifiedSchedulingEngine"` + Volatile bool `json:"Volatile"` + WakeToRun bool `json:"WakeToRun"` } type ScheduledTaskAction struct { - Execute string `json:"Execute"` - Arguments string `json:"Arguments"` + Arguments string `json:"Arguments"` + Execute string `json:"Execute"` + ID string `json:"Id"` + PSComputerName string `json:"PSComputerName"` + WorkingDirectory string `json:"WorkingDirectory"` } diff --git a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 index 2d4f35e8..f792e735 100644 --- a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 +++ b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 @@ -1,45 +1,148 @@ # list scheduled tasks in json format -# usage: .\scheduled_tasks.ps1 [-title ] -# +# usage: .\scheduled_tasks.ps1 [-title ] [-folder ] [-recursive ] -if (!$title) { $title = $null } +# Parse named arguments (for standalone invocation). +# When called via snclient, variables are injected at the top of the script instead, +# so $args will be empty and this loop does nothing. +if ($args) { + for ($i = 0; $i -lt $args.Count; $i++) { + if ($args[$i] -eq '-title' -and $i + 1 -lt $args.Count) { + $title = $args[$i + 1] + $i++ + continue + } + if ($args[$i] -eq '-folder' -and $i + 1 -lt $args.Count) { + $folder = $args[$i + 1] + $i++ + continue + } + if ($args[$i] -eq '-recursive' -and $i + 1 -lt $args.Count) { + $recursive = $args[$i + 1] + $i++ + continue + } + } +} + +# Apply defaults when variables are not defined (neither by snclient injection nor by args) +if (!$title) { $title = '*' } +if (!$folder) { $folder = '\' } +if (!$recursive) { $recursive = 'false' } # ensure output is utf8 $OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 -if ($title) { - $tasks = Get-ScheduledTask -TaskName ('*' + $title + '*') +$params = @{} +if ($title -ne '*') { + $params.TaskName = '*' + $title + '*' +} +if ($recursive -eq 'true') { + $params.TaskPath = $folder + '*' } else { - $tasks = Get-ScheduledTask + $params.TaskPath = $folder +} + +try { + $tasks = Get-ScheduledTask @params -ErrorAction Stop +} catch { + $tasks = @() } -$tasks | ForEach-Object { - $task = $_ +$results = @() +foreach ($task in $tasks) { $taskInfo = Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath # Extract command line and arguments from the actions + # Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Actions | Get-Member -MemberType Property $actions = @($task.Actions | ForEach-Object { [PSCustomObject]@{ - Execute = $_.Execute Arguments = $_.Arguments + Execute = $_.Execute + Id = $_.Id + PSComputerName = $_.PSComputerName + WorkingDirectory = $_.WorkingDirectory } }) + # Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Triggers | Get-Member -MemberType Property + $triggers = @($task.Triggers | ForEach-Object { + [PSCustomObject]@{ + DaysInterval = $_.DaysInterval + Enabled = $_.Enabled + EndBoundary = $_.EndBoundary + ExecutionTimeLimit = $_.ExecutionTimeLimit + Id = $_.Id + RandomDelay = $_.RandomDelay + Repetition = $_.Repetition + StartBoundary = $_.StartBoundary + } + }) + + # Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Settings | Get-Member -MemberType Property + $settings = [PSCustomObject]@{ + AllowDemandStart = $task.Settings.AllowDemandStart + AllowHardTerminate = $task.Settings.AllowHardTerminate + DeleteExpiredTaskAfter = $task.Settings.DeleteExpiredTaskAfter + DisallowStartIfOnBatteries = $task.Settings.DisallowStartIfOnBatteries + DisallowStartOnRemoteAppSession = $task.Settings.DisallowStartOnRemoteAppSession + Enabled = $task.Settings.Enabled + ExecutionTimeLimit = $task.Settings.ExecutionTimeLimit + Hidden = $task.Settings.Hidden + IdleSettings = $task.Settings.IdleSettings + MaintenanceSettings = $task.Settings.MaintenanceSettings + NetworkSettings = $task.Settings.NetworkSettings + Priority = $task.Settings.Priority + PSComputerName = $task.Settings.PSComputerName + RestartCount = $task.Settings.RestartCount + RestartInterval = $task.Settings.RestartInterval + RunOnlyIfIdle = $task.Settings.RunOnlyIfIdle + RunOnlyIfNetworkAvailable = $task.Settings.RunOnlyIfNetworkAvailable + StartWhenAvailable = $task.Settings.StartWhenAvailable + StopIfGoingOnBatteries = $task.Settings.StopIfGoingOnBatteries + UseUnifiedSchedulingEngine = $task.Settings.UseUnifiedSchedulingEngine + Volatile = $task.Settings.Volatile + WakeToRun = $task.Settings.WakeToRun + } + + # Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Principal | Get-Member -MemberType Property + $principal = [PSCustomObject]@{ + DisplayName = $task.Principal.DisplayName + Id = $task.Principal.Id + GroupId = $task.Principal.GroupId + PSComputerName = $task.Principal.PSComputerName + RequiredPrivilege = $task.Principal.RequiredPrivilege + UserId = $task.Principal.UserId + } + # Combine task properties with task info properties - [PSCustomObject]@{ - TaskName = $task.TaskName - TaskPath = $task.TaskPath - State = $task.State - Description = $task.Description - LastRunTime = $taskInfo.LastRunTime - NextRunTime = $taskInfo.NextRunTime - LastTaskResult = $taskInfo.LastTaskResult - MissedRuns = $taskInfo.NumberOfMissedRuns - Author = $task.Principal.UserId - Enabled = $task.Settings.Enabled - Priority = $task.Settings.Priority - Hidden = $task.Settings.Hidden - TimeLimit = $task.Settings.ExecutionTimeLimit - Actions = $actions + # Get-ScheduledTask -TaskName "XYZ" | Get-Member -MemberType Property + # Get-ScheduledTaskInfo -TaskName "XYZ" | Get-Member -MemberType Property + $results += [PSCustomObject]@{ + TaskName = $task.TaskName + TaskPath = $task.TaskPath + State = $task.State + Description = $task.Description + PSComputerName = $task.PSComputerName + URI = $task.URI + Version = $task.Version + LastRunTime = $taskInfo.LastRunTime + LastTaskResult = $taskInfo.LastTaskResult + NextRunTime = $taskInfo.NextRunTime + NumberOfMissedRuns = $taskInfo.NumberOfMissedRuns + UserId = $principal.UserId + Enabled = $settings.Enabled + Priority = $settings.Priority + Hidden = $settings.Hidden + ExecutionTimeLimit = $settings.ExecutionTimeLimit + Principal = $principal + Actions = $actions + Triggers = $triggers + Settings = $settings } -} | ConvertTo-Json -Depth 4 +} + +if ($results.Count -gt 0) { + ConvertTo-Json -InputObject $results -Depth 4 +} else { + '[]' +} From c7cf330d78a3dd550a89e32c236414315e494f37 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Mon, 15 Jun 2026 15:40:34 +0200 Subject: [PATCH 05/14] check_tasksched: recursive is on by default --- pkg/snclient/check_tasksched.go | 6 +++--- pkg/snclient/check_tasksched_windows.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index 140256a5..dc366b08 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -13,7 +13,7 @@ func init() { type CheckTasksched struct { TaskTitle string Folder string - Rerursive bool + Recursive bool } const ( @@ -24,7 +24,7 @@ func NewCheckTasksched() CheckHandler { return &CheckTasksched{ TaskTitle: CheckTaskschedDefaultTaskTitle, Folder: "\\", - Rerursive: false, + Recursive: true, } } @@ -40,7 +40,7 @@ func (l *CheckTasksched) Build() *CheckData { "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 task, used when iteratating over tasks."}, "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.Rerursive, description: "Include the subfolders of the specified folder as well when searching for scheduled tasks."}, + "recursive": {value: &l.Recursive, description: "Include the subfolders of the specified folder as well when searching for scheduled tasks."}, }, defaultFilter: "enabled = true", defaultCritical: "exit_code < 0", diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index c54ca421..58199a3a 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -27,7 +27,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD if err != nil { return err } - err = powerShellCmdAddVariableDefinition(cmd, "recursive", strconv.FormatBool(l.Rerursive)) + err = powerShellCmdAddVariableDefinition(cmd, "recursive", strconv.FormatBool(l.Recursive)) if err != nil { return err } From c750434e9492a3e363b7284f4f53d51e46c96e38 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Mon, 15 Jun 2026 15:52:41 +0200 Subject: [PATCH 06/14] check_tasksched: citest fixes --- docs/checks/commands/check_tasksched.md | 10 +++-- pkg/snclient/check_tasksched_windows.go | 60 ++++++++++++------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/docs/checks/commands/check_tasksched.md b/docs/checks/commands/check_tasksched.md index 0309f4eb..ee314d1d 100644 --- a/docs/checks/commands/check_tasksched.md +++ b/docs/checks/commands/check_tasksched.md @@ -54,10 +54,12 @@ Naemon Config ## Check Specific Arguments -| Argument | Description | -| -------- | --------------------------------------------------------------------------------------------------- | -| timezone | Sets the timezone for time metrics (default is local time) | -| title | Sets the task to check. This corresonds to the title of the task, used when iteratating over tasks. | +| 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. | +| timezone | Sets the timezone for time metrics (default is local time) | +| title | Sets the task to check. This corresonds to the title of the task, used when iteratating over tasks. | ## Attributes diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 58199a3a..ad2d7a61 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -239,39 +239,39 @@ type ScheduledTaskPrincipal struct { } type ScheduledTaskTrigger struct { - DaysInterval int64 `json:"DaysInterval"` - Enabled bool `json:"Enabled"` - EndBoundary string `json:"EndBoundary"` - ExecutionTimeLimit string `json:"ExecutionTimeLimit"` - ID string `json:"Id"` - RandomDelay string `json:"RandomDelay"` - Repetition interface{} `json:"Repetition"` - StartBoundary string `json:"StartBoundary"` + DaysInterval int64 `json:"DaysInterval"` + Enabled bool `json:"Enabled"` + EndBoundary string `json:"EndBoundary"` + ExecutionTimeLimit string `json:"ExecutionTimeLimit"` + ID string `json:"Id"` + RandomDelay string `json:"RandomDelay"` + Repetition any `json:"Repetition"` + StartBoundary string `json:"StartBoundary"` } type ScheduledTaskSetting struct { - AllowDemandStart bool `json:"AllowDemandStart"` - AllowHardTerminate bool `json:"AllowHardTerminate"` - DeleteExpiredTaskAfter string `json:"DeleteExpiredTaskAfter"` - DisallowStartIfOnBatteries bool `json:"DisallowStartIfOnBatteries"` - DisallowStartOnRemoteAppSession bool `json:"DisallowStartOnRemoteAppSession"` - Enabled bool `json:"Enabled"` - ExecutionTimeLimit string `json:"ExecutionTimeLimit"` - Hidden bool `json:"Hidden"` - IdleSettings interface{} `json:"IdleSettings"` - MaintenanceSettings interface{} `json:"MaintenanceSettings"` - NetworkSettings interface{} `json:"NetworkSettings"` - Priority int64 `json:"Priority"` - PSComputerName string `json:"PSComputerName"` - RestartCount int64 `json:"RestartCount"` - RestartInterval string `json:"RestartInterval"` - RunOnlyIfIdle bool `json:"RunOnlyIfIdle"` - RunOnlyIfNetworkAvailable bool `json:"RunOnlyIfNetworkAvailable"` - StartWhenAvailable bool `json:"StartWhenAvailable"` - StopIfGoingOnBatteries bool `json:"StopIfGoingOnBatteries"` - UseUnifiedSchedulingEngine bool `json:"UseUnifiedSchedulingEngine"` - Volatile bool `json:"Volatile"` - WakeToRun bool `json:"WakeToRun"` + AllowDemandStart bool `json:"AllowDemandStart"` + AllowHardTerminate bool `json:"AllowHardTerminate"` + DeleteExpiredTaskAfter string `json:"DeleteExpiredTaskAfter"` + DisallowStartIfOnBatteries bool `json:"DisallowStartIfOnBatteries"` + DisallowStartOnRemoteAppSession bool `json:"DisallowStartOnRemoteAppSession"` + Enabled bool `json:"Enabled"` + ExecutionTimeLimit string `json:"ExecutionTimeLimit"` + Hidden bool `json:"Hidden"` + IdleSettings any `json:"IdleSettings"` + MaintenanceSettings any `json:"MaintenanceSettings"` + NetworkSettings any `json:"NetworkSettings"` + Priority int64 `json:"Priority"` + PSComputerName string `json:"PSComputerName"` + RestartCount int64 `json:"RestartCount"` + RestartInterval string `json:"RestartInterval"` + RunOnlyIfIdle bool `json:"RunOnlyIfIdle"` + RunOnlyIfNetworkAvailable bool `json:"RunOnlyIfNetworkAvailable"` + StartWhenAvailable bool `json:"StartWhenAvailable"` + StopIfGoingOnBatteries bool `json:"StopIfGoingOnBatteries"` + UseUnifiedSchedulingEngine bool `json:"UseUnifiedSchedulingEngine"` + Volatile bool `json:"Volatile"` + WakeToRun bool `json:"WakeToRun"` } type ScheduledTaskAction struct { From 421653e19db7c88de2598f5c01d7a2da8ffeb3f6 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 16 Jun 2026 10:41:08 +0200 Subject: [PATCH 07/14] check_tasksched; pass parameters to script instead of defining them powershellCmd now takes a variadic parameter argument, these get added to the commandline. instead of defining these variables in the command body, now they are properly passed as parameters to the script. the command body is prepended with parameter definitions, and the powershell commandline is appended with parameter specifications --- pkg/snclient/check_tasksched.go | 8 ++-- pkg/snclient/check_tasksched_windows.go | 40 +++++++++++------ pkg/snclient/snclient_windows.go | 58 ++++++++++++++----------- 3 files changed, 64 insertions(+), 42 deletions(-) diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index dc366b08..0cc0ca62 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -18,13 +18,15 @@ type CheckTasksched struct { const ( CheckTaskschedDefaultTaskTitle string = "*" + CheckTaskschedDefaultFolder string = "\\" + CheckTaskschedDefaultRecursive bool = true ) func NewCheckTasksched() CheckHandler { return &CheckTasksched{ TaskTitle: CheckTaskschedDefaultTaskTitle, - Folder: "\\", - Recursive: true, + Folder: CheckTaskschedDefaultFolder, + Recursive: CheckTaskschedDefaultRecursive, } } @@ -38,7 +40,7 @@ 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: "Sets the task to check. This corresonds to the title of the task, used when iteratating over tasks."}, + "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."}, }, diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 58199a3a..64b5583c 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -18,19 +18,33 @@ var scheduledTasksPS1 string func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error { script := scheduledTasksPS1 - cmd := powerShellCmd(ctx, script) - err := powerShellCmdAddVariableDefinition(cmd, "title", l.TaskTitle) - if err != nil { - return err - } - err = powerShellCmdAddVariableDefinition(cmd, "folder", l.Folder) - if err != nil { - return err - } - err = powerShellCmdAddVariableDefinition(cmd, "recursive", strconv.FormatBool(l.Recursive)) - if err != nil { - return err - } + + cmd := 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), + }, + ) output, stderr, exitCode, _, err := snc.runExternalCommand(ctx, cmd, snc.getBuiltinCmdTimeout()) if err != nil { diff --git a/pkg/snclient/snclient_windows.go b/pkg/snclient/snclient_windows.go index 2ef86c35..90d925ef 100644 --- a/pkg/snclient/snclient_windows.go +++ b/pkg/snclient/snclient_windows.go @@ -393,42 +393,48 @@ func shell() string { return shell } -func powerShellCmd(ctx context.Context, command string) (cmd *exec.Cmd) { +type PowerShellParameter struct { + name string + parameterType string + specifyDefaultValue bool + defaultValue string + specifyValue bool + specifiedValue string +} + +func powerShellCmd(ctx context.Context, command string, parameters ...PowerShellParameter) (cmd *exec.Cmd) { cmd = exec.CommandContext(ctx, "powershell") cmd.Args = nil - cmdLine := fmt.Sprintf(`%s -Command "%s"`, POWERSHELL, command) //nolint:gocritic // using %q just breaks the command from escaping newlines - cmd.SysProcAttr = &syscall.SysProcAttr{ - HideWindow: true, - CmdLine: cmdLine, - } - return cmd -} + // powershellInvocationArguments "& { param([string]$param1='defaultValue1', [string]$param2='defaultValue2') scriptContent }" -param1 "value1" -param2 "value2" -// this function finds where the powershell command body starts, and prepends a variable definition with a value. -func powerShellCmdAddVariableDefinition(cmd *exec.Cmd, variableName, variableValue string) (err error) { - if cmd == nil { - return errors.New("powershell command that was passed is nil") - } - if cmd.SysProcAttr.CmdLine == "" { - return errors.New("powershell command has empty SysProcAttr.CmdLine. This function can only modify it when its present") + parameterDefinitions := make([]string, 0, len(parameters)) + for _, para := range parameters { + def := fmt.Sprintf("[%s]$%s", para.parameterType, para.name) + if para.specifyDefaultValue { + def += fmt.Sprintf("='%s'", para.defaultValue) + } + parameterDefinitions = append(parameterDefinitions, def) } + parameterDefinitionsCmdline := fmt.Sprintf("param(%s)", strings.Join(parameterDefinitions, ", ")) - cmdline := cmd.SysProcAttr.CmdLine - cmdlinePrefix := fmt.Sprintf(`%s -Command "`, POWERSHELL) - - if cut, cutOk := strings.CutPrefix(cmdline, cmdlinePrefix); cutOk { - newCmdline := cmdlinePrefix + - fmt.Sprintf(`$%s = '%s';`, variableName, strings.ReplaceAll(variableValue, "'", "''")) + - "\r\n" + - cut + parameterSpecifications := make([]string, 0, len(parameters)) + for _, para := range parameters { + if para.specifyValue { + spec := fmt.Sprintf(`-%s '%s'`, para.name, para.specifiedValue) + parameterSpecifications = append(parameterSpecifications, spec) + } + } + parameterSpecificationsCmdline := strings.Join(parameterSpecifications, " ") - cmd.SysProcAttr.CmdLine = newCmdline + cmdLine := fmt.Sprintf(`%s -Command "& { %s %s }" %s `, POWERSHELL, parameterDefinitionsCmdline, command, parameterSpecificationsCmdline) - return nil + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + CmdLine: cmdLine, } - return errors.New("powershell command does not start with the expected prefix") + return cmd } func isBatchFile(path string) bool { From 90e77fc706a151e5cd9af9ad349a6eaf0d82cac9 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 16 Jun 2026 10:42:03 +0200 Subject: [PATCH 08/14] check_tasksched: the title parameter must now match the task name exactly --- pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 index f792e735..27ffd1ab 100644 --- a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 +++ b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 @@ -34,7 +34,7 @@ $OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 $params = @{} if ($title -ne '*') { - $params.TaskName = '*' + $title + '*' + $params.TaskName = $title } if ($recursive -eq 'true') { $params.TaskPath = $folder + '*' From 56fe1bd3f0e51f8abaacd9774be8da62571d3972 Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Tue, 16 Jun 2026 10:43:29 +0200 Subject: [PATCH 09/14] check_tasksched: use the URI of task in the detail syntax. better than using the task name, which might be same. uri behaves more like a fully qualified name --- pkg/snclient/check_tasksched.go | 2 +- pkg/snclient/check_tasksched_windows.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index 0cc0ca62..2c9b31b8 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -47,7 +47,7 @@ func (l *CheckTasksched) Build() *CheckData { defaultFilter: "enabled = true", defaultCritical: "exit_code < 0", defaultWarning: "exit_code != 0", - detailSyntax: "${folder}/${title}: ${exit_code} != 0", + detailSyntax: "${uri}: ${exit_code} != 0", topSyntax: "%(status) - ${problem_list}", okSyntax: "%(status) - All tasks are ok", emptySyntax: "%(status) - No tasks found", diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 64b5583c..a8d786a9 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -76,6 +76,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD "exit_code": fmt.Sprintf("%d", task.LastTaskResult.exitCode()), "exit_string": task.LastTaskResult.String(), "folder": task.Path, + "uri": task.URI, "has_run": fmt.Sprintf("%t", hasRun), "max_run_time": task.ExecutionTimeLimit, "most_recent_run_time": fmt.Sprintf("%d", l.parseDate(task.LastRunTime).Unix()), From 20122aa6d86d23ee3fe614a3c7d7ef31d2967944 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Tue, 16 Jun 2026 10:54:47 +0200 Subject: [PATCH 10/14] check_tasksched: citest fixes --- docs/checks/commands/check_tasksched.md | 22 +++++++++++----------- pkg/snclient/check_tasksched_windows.go | 3 ++- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/checks/commands/check_tasksched.md b/docs/checks/commands/check_tasksched.md index ee314d1d..986abcdc 100644 --- a/docs/checks/commands/check_tasksched.md +++ b/docs/checks/commands/check_tasksched.md @@ -41,16 +41,16 @@ Naemon Config ## Argument Defaults -| Argument | Default Value | -| ------------- | ---------------------------------------- | -| filter | enabled = true | -| warning | exit_code != 0 | -| critical | exit_code < 0 | -| empty-state | 1 (WARNING) | -| empty-syntax | %(status) - No tasks found | -| top-syntax | %(status) - \${problem_list} | -| ok-syntax | %(status) - All tasks are ok | -| detail-syntax | \${folder}/\${title}: \${exit_code} != 0 | +| Argument | Default Value | +| ------------- | ---------------------------- | +| filter | enabled = true | +| warning | exit_code != 0 | +| critical | exit_code < 0 | +| empty-state | 1 (WARNING) | +| empty-syntax | %(status) - No tasks found | +| top-syntax | %(status) - \${problem_list} | +| ok-syntax | %(status) - All tasks are ok | +| detail-syntax | \${uri}: \${exit_code} != 0 | ## Check Specific Arguments @@ -59,7 +59,7 @@ Naemon Config | 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. | | timezone | Sets the timezone for time metrics (default is local time) | -| title | Sets the task to check. This corresonds to the title of the task, used when iteratating over tasks. | +| title | Sets the task to check. This corresonds to the title of the scheduled task, called TaskName in Powershell output. | ## Attributes diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 25b2a8a8..1bf34791 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -19,7 +19,8 @@ var scheduledTasksPS1 string func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error { script := scheduledTasksPS1 - cmd := powerShellCmd(ctx, script, + cmd := powerShellCmd( + ctx, script, PowerShellParameter{ name: "title", parameterType: "string", From 537f778df36f8842bd577dacc47815a79361309f Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Wed, 17 Jun 2026 13:19:03 +0200 Subject: [PATCH 11/14] add uri_clean attribute, removes the leading backslash for uris that have only one leading backslash i.e saved at root change the detail syntax, use uri_clean , include a formatted most_recent_run_time and print the exit code directly change default empty state to unknown for cases where user specifies title in filters, thresholds or directly in the arguent, change the empty result syntax and warn about title attribute usage --- pkg/snclient/check_tasksched.go | 6 ++++-- pkg/snclient/check_tasksched_windows.go | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index 2c9b31b8..183d09cc 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -47,11 +47,11 @@ func (l *CheckTasksched) Build() *CheckData { defaultFilter: "enabled = true", defaultCritical: "exit_code < 0", defaultWarning: "exit_code != 0", - detailSyntax: "${uri}: ${exit_code} != 0", + detailSyntax: "${uri_clean} (%{most_recent_run_time:date}) exited with ${exit_code}", topSyntax: "%(status) - ${problem_list}", okSyntax: "%(status) - All tasks are ok", emptySyntax: "%(status) - No tasks found", - emptyState: CheckExitWarning, + emptyState: CheckExitUnknown, attributes: []CheckAttribute{ {name: "application", description: "Name of the application that the task is associated with"}, {name: "comment", description: "Comment or description for the work item"}, @@ -60,6 +60,8 @@ func (l *CheckTasksched) Build() *CheckData { {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: "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}, diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 1bf34791..11c02744 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -7,6 +7,7 @@ import ( _ "embed" "fmt" "strconv" + "strings" "syscall" "time" @@ -78,6 +79,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD "exit_string": task.LastTaskResult.String(), "folder": task.Path, "uri": task.URI, + "uri_clean": parseURIClean(task.URI), "has_run": fmt.Sprintf("%t", hasRun), "max_run_time": task.ExecutionTimeLimit, "most_recent_run_time": fmt.Sprintf("%d", l.parseDate(task.LastRunTime).Unix()), @@ -92,9 +94,24 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD check.listData = append(check.listData, entry) } + if check.HasThreshold("title") || check.hasThresholdCond(check.filter, "title") || l.TaskTitle != CheckTaskschedDefaultTaskTitle { + check.emptyState = CheckExitUnknown + check.emptySyntax = "%(status) - No tasks found, check your arguments/filters/thresholds using title attribute." + } + return nil } +func parseURIClean(uri string) string { + if strings.Count(uri, "\\") == 1 { + if cut, cutOk := strings.CutPrefix(uri, "\\"); cutOk { + return cut + } + } + + return uri +} + func (l *CheckTasksched) parseDate(raw string) time.Time { // date matches the pattern /Date(unixmilliseconds))/ if len(raw) > 6 && raw[0:6] == "/Date(" { From 3aa8b6224fd14c7803849457b92451bbc5497dc7 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Wed, 17 Jun 2026 13:26:58 +0200 Subject: [PATCH 12/14] check_tasksched: citest fixes --- docs/checks/commands/check_tasksched.md | 60 +++++++++++++------------ pkg/snclient/check_tasksched_windows.go | 1 + 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/docs/checks/commands/check_tasksched.md b/docs/checks/commands/check_tasksched.md index 986abcdc..4776da3e 100644 --- a/docs/checks/commands/check_tasksched.md +++ b/docs/checks/commands/check_tasksched.md @@ -41,16 +41,16 @@ Naemon Config ## Argument Defaults -| Argument | Default Value | -| ------------- | ---------------------------- | -| filter | enabled = true | -| warning | exit_code != 0 | -| critical | exit_code < 0 | -| empty-state | 1 (WARNING) | -| empty-syntax | %(status) - No tasks found | -| top-syntax | %(status) - \${problem_list} | -| ok-syntax | %(status) - All tasks are ok | -| detail-syntax | \${uri}: \${exit_code} != 0 | +| Argument | Default Value | +| ------------- | ---------------------------------------------------------------------- | +| filter | enabled = true | +| warning | exit_code != 0 | +| critical | exit_code < 0 | +| empty-state | 3 (UNKNOWN) | +| empty-syntax | %(status) - No tasks found | +| top-syntax | %(status) - \${problem_list} | +| ok-syntax | %(status) - All tasks are ok | +| detail-syntax | \${uri_clean} (%{most_recent_run_time:date}) exited with \${exit_code} | ## Check Specific Arguments @@ -67,22 +67,24 @@ Naemon Config these can be used in filters and thresholds (along with the default attributes): -| Attribute | Description | -| -------------------- | ------------------------------------------------------------------ | -| application | Name of the application that the task is associated with | -| comment | Comment or description for the work item | -| creator | Creator of the work item | -| enabled | Flag whether this job is enabled (true/false) | -| exit_code | The last jobs exit code | -| exit_string | The last jobs exit code as string | -| folder | Task folder | -| has_run | True if this task has ever been executed | -| max_run_time | Maximum length of time the task can run | -| most_recent_run_time | Most recent time the work item began running | -| priority | Task priority | -| title | Task title | -| hidden | Indicates that the task will not be visible in the UI (true/false) | -| missed_runs | Number of times the registered task has missed a scheduled run | -| task_status | Task status as string | -| next_run_time | Time when the registered task is next scheduled to run | -| parameters | Command line parameters for the task | +| Attribute | Description | +| -------------------- | ---------------------------------------------------------------------------------------------- | +| application | Name of the application that the task is associated with | +| comment | Comment or description for the work item | +| creator | Creator of the work item | +| enabled | Flag whether this job is enabled (true/false) | +| exit_code | The last jobs exit code | +| exit_string | The last jobs exit code as string | +| folder | Task folder | +| uri | Fully qualified path to the task, includes folder and the task title | +| uri_clean | Remove the leading backslash from the URI, only for tasks directly saved at root and not for ones saved inside folders. | +| has_run | True if this task has ever been executed | +| max_run_time | Maximum length of time the task can run | +| most_recent_run_time | Most recent time the work item began running | +| priority | Task priority | +| title | Task title | +| hidden | Indicates that the task will not be visible in the UI (true/false) | +| missed_runs | Number of times the registered task has missed a scheduled run | +| task_status | Task status as string | +| next_run_time | Time when the registered task is next scheduled to run | +| parameters | Command line parameters for the task | diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 11c02744..1826f101 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -17,6 +17,7 @@ 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 From 7d464d5afc3cf932ad84d432fd8d4efa07c3d79a Mon Sep 17 00:00:00 2001 From: Ahmet Ozturk Date: Thu, 18 Jun 2026 15:29:55 +0200 Subject: [PATCH 13/14] check_tasksched: address comments checker for the custom specified Folder and title, prevents powershell explotations check passed parameters for quoutes when building powershell command. with another single quoute, existing quoute can be escaped and custom commands can be run print output if the exitcode is 0 in the returned error properly add paramters, executed command, and working dir , was not being added before comment out discovered but unused fields returned while discovering scheduled tasks. when needed, they can be uncommented and used. --- pkg/snclient/check_os_updates_windows.go | 5 +- pkg/snclient/check_tasksched.go | 4 +- pkg/snclient/check_tasksched_windows.go | 196 ++++++++++++------ .../embed/scripts/windows/scheduled_tasks.ps1 | 106 +++++----- pkg/snclient/snclient_windows.go | 19 +- 5 files changed, 204 insertions(+), 126 deletions(-) diff --git a/pkg/snclient/check_os_updates_windows.go b/pkg/snclient/check_os_updates_windows.go index 703fb577..d606491c 100644 --- a/pkg/snclient/check_os_updates_windows.go +++ b/pkg/snclient/check_os_updates_windows.go @@ -41,7 +41,10 @@ func (l *CheckOSUpdates) addWindows(ctx context.Context, check *CheckData) (bool // https://learn.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdatesearcher-search // https://learn.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdate - cmd := powerShellCmd(ctx, checkOSupdatesPS1) + cmd, err := powerShellCmd(ctx, checkOSupdatesPS1) + if err != nil { + return false, fmt.Errorf("error when building a powershell command: %s", err.Error()) + } if l.update { cmd.Env = append(cmd.Env, "ONLINE_SEARCH=1") } diff --git a/pkg/snclient/check_tasksched.go b/pkg/snclient/check_tasksched.go index 183d09cc..fd0beea8 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -71,7 +71,9 @@ func (l *CheckTasksched) Build() *CheckData { {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}, - {name: "parameters", description: "Command line parameters for the task"}, + {name: "parameters", description: "Last actions command line parameters"}, + {name: "execute", description: "Last actions executed program"}, + {name: "working_dir", 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 1826f101..1c0ab7ca 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -10,6 +10,7 @@ import ( "strings" "syscall" "time" + "unicode" "github.com/goccy/go-json" ) @@ -21,7 +22,26 @@ var scheduledTasksPS1 string func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error { script := scheduledTasksPS1 - cmd := powerShellCmd( + // 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 + } + } + + if l.TaskTitle != CheckTaskschedDefaultTaskTitle { + if strings.ContainsFunc(l.TaskTitle, func(r rune) bool { return !unicode.IsLetter(r) }) { + return fmt.Errorf("custom specified title should be all letters, but it isnt: %s", l.TaskTitle) + } + } + + if l.Folder != CheckTaskschedDefaultFolder { + if strings.ContainsFunc(l.Folder, func(r rune) bool { return !unicode.IsLetter(r) && r != '\\' }) { + return fmt.Errorf("custom specified folder should be all letters or backslashes, but it isnt: %s", l.Folder) + } + } + + cmd, err := powerShellCmd( ctx, script, PowerShellParameter{ name: "title", @@ -49,12 +69,16 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD }, ) + if err != nil { + return fmt.Errorf("error when building a powershell command: %s", err.Error()) + } + output, stderr, exitCode, _, err := snc.runExternalCommand(ctx, cmd, snc.getBuiltinCmdTimeout()) if err != nil { return fmt.Errorf("getting scheduled tasks failed, error: %s\n%s", err.Error(), stderr) } if exitCode != 0 { - return fmt.Errorf("getting scheduled tasks failed, exitCode: %d\n%s", exitCode, stderr) + return fmt.Errorf("getting scheduled tasks failed, exitCode: %d, output: %s\n%s", exitCode, output, stderr) } var taskList []ScheduledTask @@ -69,7 +93,6 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD if task.LastRunTime != "" { hasRun = true } - parameters := "" entry := map[string]string{ "application": task.Name, @@ -90,7 +113,9 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD "missed_runs": fmt.Sprintf("%d", task.NumberOfMissedRuns), "task_status": task.State.String(), "next_run_time": fmt.Sprintf("%d", l.parseDate(task.NextRunTime).Unix()), - "parameters": parameters, + "parameters": l.parseParameters(task.Actions), + "execute": l.parseExecuteCmd(task.Actions), + "working_dir": l.parseWorkingDir(task.Actions), } check.listData = append(check.listData, entry) } @@ -125,6 +150,30 @@ func (l *CheckTasksched) parseDate(raw string) time.Time { return time.Time{} } +func (l *CheckTasksched) parseParameters(actions []ScheduledTaskAction) string { + if len(actions) == 0 { + return "" + } + + return actions[len(actions)-1].Arguments +} + +func (l *CheckTasksched) parseExecuteCmd(actions []ScheduledTaskAction) string { + if len(actions) == 0 { + return "" + } + + return actions[len(actions)-1].Execute +} + +func (l *CheckTasksched) parseWorkingDir(actions []ScheduledTaskAction) string { + if len(actions) == 0 { + return "" + } + + return actions[len(actions)-1].WorkingDirectory +} + type TaskResult uint32 // https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-error-and-success-constants @@ -240,72 +289,30 @@ func (t TaskState) String() string { } } -type ScheduledTask struct { - Name string `json:"TaskName"` - Path string `json:"TaskPath"` - Description string `json:"Description"` - PSComputerName string `json:"PSComputerName"` - URI string `json:"URI"` - Version string `json:"Version"` - LastRunTime string `json:"LastRunTime"` - State TaskState `json:"State"` - NextRunTime string `json:"NextRunTime"` - LastTaskResult TaskResult `json:"LastTaskResult"` - NumberOfMissedRuns int64 `json:"NumberOfMissedRuns"` - UserID string `json:"UserId"` - Enabled bool `json:"Enabled"` - Priority int64 `json:"Priority"` - Hidden bool `json:"Hidden"` - ExecutionTimeLimit string `json:"ExecutionTimeLimit"` - Principal ScheduledTaskPrincipal `json:"Principal"` - Actions []ScheduledTaskAction `json:"Actions"` - Triggers []ScheduledTaskTrigger `json:"Triggers"` - Settings ScheduledTaskSetting `json:"Settings"` -} - -type ScheduledTaskPrincipal struct { - DisplayName string `json:"DisplayName"` - ID string `json:"Id"` - GroupID string `json:"GroupId"` - PSComputerName string `json:"PSComputerName"` - RequiredPrivilege []string `json:"RequiredPrivilege"` - UserID string `json:"UserId"` -} - -type ScheduledTaskTrigger struct { - DaysInterval int64 `json:"DaysInterval"` - Enabled bool `json:"Enabled"` - EndBoundary string `json:"EndBoundary"` - ExecutionTimeLimit string `json:"ExecutionTimeLimit"` - ID string `json:"Id"` - RandomDelay string `json:"RandomDelay"` - Repetition any `json:"Repetition"` - StartBoundary string `json:"StartBoundary"` -} +// The script does not export everything it discovers to JSON for snclient to parse +// When needed, modify the script and uncomment these lines -type ScheduledTaskSetting struct { - AllowDemandStart bool `json:"AllowDemandStart"` - AllowHardTerminate bool `json:"AllowHardTerminate"` - DeleteExpiredTaskAfter string `json:"DeleteExpiredTaskAfter"` - DisallowStartIfOnBatteries bool `json:"DisallowStartIfOnBatteries"` - DisallowStartOnRemoteAppSession bool `json:"DisallowStartOnRemoteAppSession"` - Enabled bool `json:"Enabled"` - ExecutionTimeLimit string `json:"ExecutionTimeLimit"` - Hidden bool `json:"Hidden"` - IdleSettings any `json:"IdleSettings"` - MaintenanceSettings any `json:"MaintenanceSettings"` - NetworkSettings any `json:"NetworkSettings"` - Priority int64 `json:"Priority"` - PSComputerName string `json:"PSComputerName"` - RestartCount int64 `json:"RestartCount"` - RestartInterval string `json:"RestartInterval"` - RunOnlyIfIdle bool `json:"RunOnlyIfIdle"` - RunOnlyIfNetworkAvailable bool `json:"RunOnlyIfNetworkAvailable"` - StartWhenAvailable bool `json:"StartWhenAvailable"` - StopIfGoingOnBatteries bool `json:"StopIfGoingOnBatteries"` - UseUnifiedSchedulingEngine bool `json:"UseUnifiedSchedulingEngine"` - Volatile bool `json:"Volatile"` - WakeToRun bool `json:"WakeToRun"` +type ScheduledTask struct { + Name string `json:"TaskName"` + Path string `json:"TaskPath"` + Description string `json:"Description"` + PSComputerName string `json:"PSComputerName"` + URI string `json:"URI"` + Version string `json:"Version"` + LastRunTime string `json:"LastRunTime"` + State TaskState `json:"State"` + NextRunTime string `json:"NextRunTime"` + LastTaskResult TaskResult `json:"LastTaskResult"` + NumberOfMissedRuns int64 `json:"NumberOfMissedRuns"` + UserID string `json:"UserId"` + Enabled bool `json:"Enabled"` + Priority int64 `json:"Priority"` + Hidden bool `json:"Hidden"` + ExecutionTimeLimit string `json:"ExecutionTimeLimit"` + Actions []ScheduledTaskAction `json:"Actions"` + // Principal ScheduledTaskPrincipal `json:"Principal"` + // Triggers []ScheduledTaskTrigger `json:"Triggers"` + // Settings ScheduledTaskSetting `json:"Settings"` } type ScheduledTaskAction struct { @@ -315,3 +322,54 @@ type ScheduledTaskAction struct { PSComputerName string `json:"PSComputerName"` WorkingDirectory string `json:"WorkingDirectory"` } + +// The script does not export everything it discovers to JSON for snclient to parse +// When needed, modify the script and uncomment these lines +// type ScheduledTaskPrincipal struct { +// DisplayName string `json:"DisplayName"` +// ID string `json:"Id"` +// GroupID string `json:"GroupId"` +// PSComputerName string `json:"PSComputerName"` +// RequiredPrivilege []string `json:"RequiredPrivilege"` +// UserID string `json:"UserId"` +// } + +// The script does not export everything it discovers to JSON for snclient to parse +// When needed, modify the script and uncomment these lines +// type ScheduledTaskTrigger struct { +// DaysInterval int64 `json:"DaysInterval"` +// Enabled bool `json:"Enabled"` +// EndBoundary string `json:"EndBoundary"` +// ExecutionTimeLimit string `json:"ExecutionTimeLimit"` +// ID string `json:"Id"` +// RandomDelay string `json:"RandomDelay"` +// Repetition any `json:"Repetition"` +// StartBoundary string `json:"StartBoundary"` +// } + +// The script does not export everything it discovers to JSON for snclient to parse +// When needed, modify the script and uncomment these lines +// type ScheduledTaskSetting struct { +// AllowDemandStart bool `json:"AllowDemandStart"` +// AllowHardTerminate bool `json:"AllowHardTerminate"` +// DeleteExpiredTaskAfter string `json:"DeleteExpiredTaskAfter"` +// DisallowStartIfOnBatteries bool `json:"DisallowStartIfOnBatteries"` +// DisallowStartOnRemoteAppSession bool `json:"DisallowStartOnRemoteAppSession"` +// Enabled bool `json:"Enabled"` +// ExecutionTimeLimit string `json:"ExecutionTimeLimit"` +// Hidden bool `json:"Hidden"` +// IdleSettings any `json:"IdleSettings"` +// MaintenanceSettings any `json:"MaintenanceSettings"` +// NetworkSettings any `json:"NetworkSettings"` +// Priority int64 `json:"Priority"` +// PSComputerName string `json:"PSComputerName"` +// RestartCount int64 `json:"RestartCount"` +// RestartInterval string `json:"RestartInterval"` +// RunOnlyIfIdle bool `json:"RunOnlyIfIdle"` +// RunOnlyIfNetworkAvailable bool `json:"RunOnlyIfNetworkAvailable"` +// StartWhenAvailable bool `json:"StartWhenAvailable"` +// StopIfGoingOnBatteries bool `json:"StopIfGoingOnBatteries"` +// UseUnifiedSchedulingEngine bool `json:"UseUnifiedSchedulingEngine"` +// Volatile bool `json:"Volatile"` +// WakeToRun bool `json:"WakeToRun"` +// } diff --git a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 index 27ffd1ab..ee9f5549 100644 --- a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 +++ b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 @@ -52,8 +52,11 @@ $results = @() foreach ($task in $tasks) { $taskInfo = Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath - # Extract command line and arguments from the actions + # Get-ScheduledTask returns a nested object + # Subobjects are not fully serialized and sent, only some of their fields are specifically selected + # Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Actions | Get-Member -MemberType Property + # This one should be exported, as a complete object. It is an array, and only the last ones execute, parameters and working directory are picked $actions = @($task.Actions | ForEach-Object { [PSCustomObject]@{ Arguments = $_.Arguments @@ -65,54 +68,54 @@ foreach ($task in $tasks) { }) # Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Triggers | Get-Member -MemberType Property - $triggers = @($task.Triggers | ForEach-Object { - [PSCustomObject]@{ - DaysInterval = $_.DaysInterval - Enabled = $_.Enabled - EndBoundary = $_.EndBoundary - ExecutionTimeLimit = $_.ExecutionTimeLimit - Id = $_.Id - RandomDelay = $_.RandomDelay - Repetition = $_.Repetition - StartBoundary = $_.StartBoundary - } - }) + # $triggers = @($task.Triggers | ForEach-Object { + # [PSCustomObject]@{ + # DaysInterval = $_.DaysInterval + # Enabled = $_.Enabled + # EndBoundary = $_.EndBoundary + # ExecutionTimeLimit = $_.ExecutionTimeLimit + # Id = $_.Id + # RandomDelay = $_.RandomDelay + # Repetition = $_.Repetition + # StartBoundary = $_.StartBoundary + # } + # }) # Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Settings | Get-Member -MemberType Property - $settings = [PSCustomObject]@{ - AllowDemandStart = $task.Settings.AllowDemandStart - AllowHardTerminate = $task.Settings.AllowHardTerminate - DeleteExpiredTaskAfter = $task.Settings.DeleteExpiredTaskAfter - DisallowStartIfOnBatteries = $task.Settings.DisallowStartIfOnBatteries - DisallowStartOnRemoteAppSession = $task.Settings.DisallowStartOnRemoteAppSession - Enabled = $task.Settings.Enabled - ExecutionTimeLimit = $task.Settings.ExecutionTimeLimit - Hidden = $task.Settings.Hidden - IdleSettings = $task.Settings.IdleSettings - MaintenanceSettings = $task.Settings.MaintenanceSettings - NetworkSettings = $task.Settings.NetworkSettings - Priority = $task.Settings.Priority - PSComputerName = $task.Settings.PSComputerName - RestartCount = $task.Settings.RestartCount - RestartInterval = $task.Settings.RestartInterval - RunOnlyIfIdle = $task.Settings.RunOnlyIfIdle - RunOnlyIfNetworkAvailable = $task.Settings.RunOnlyIfNetworkAvailable - StartWhenAvailable = $task.Settings.StartWhenAvailable - StopIfGoingOnBatteries = $task.Settings.StopIfGoingOnBatteries - UseUnifiedSchedulingEngine = $task.Settings.UseUnifiedSchedulingEngine - Volatile = $task.Settings.Volatile - WakeToRun = $task.Settings.WakeToRun - } + # $settings = [PSCustomObject]@{ + # AllowDemandStart = $task.Settings.AllowDemandStart + # AllowHardTerminate = $task.Settings.AllowHardTerminate + # DeleteExpiredTaskAfter = $task.Settings.DeleteExpiredTaskAfter + # DisallowStartIfOnBatteries = $task.Settings.DisallowStartIfOnBatteries + # DisallowStartOnRemoteAppSession = $task.Settings.DisallowStartOnRemoteAppSession + # Enabled = $task.Settings.Enabled + # ExecutionTimeLimit = $task.Settings.ExecutionTimeLimit + # Hidden = $task.Settings.Hidden + # IdleSettings = $task.Settings.IdleSettings + # MaintenanceSettings = $task.Settings.MaintenanceSettings + # NetworkSettings = $task.Settings.NetworkSettings + # Priority = $task.Settings.Priority + # PSComputerName = $task.Settings.PSComputerName + # RestartCount = $task.Settings.RestartCount + # RestartInterval = $task.Settings.RestartInterval + # RunOnlyIfIdle = $task.Settings.RunOnlyIfIdle + # RunOnlyIfNetworkAvailable = $task.Settings.RunOnlyIfNetworkAvailable + # StartWhenAvailable = $task.Settings.StartWhenAvailable + # StopIfGoingOnBatteries = $task.Settings.StopIfGoingOnBatteries + # UseUnifiedSchedulingEngine = $task.Settings.UseUnifiedSchedulingEngine + # Volatile = $task.Settings.Volatile + # WakeToRun = $task.Settings.WakeToRun + # } # Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Principal | Get-Member -MemberType Property - $principal = [PSCustomObject]@{ - DisplayName = $task.Principal.DisplayName - Id = $task.Principal.Id - GroupId = $task.Principal.GroupId - PSComputerName = $task.Principal.PSComputerName - RequiredPrivilege = $task.Principal.RequiredPrivilege - UserId = $task.Principal.UserId - } + # $principal = [PSCustomObject]@{ + # DisplayName = $task.Principal.DisplayName + # Id = $task.Principal.Id + # GroupId = $task.Principal.GroupId + # PSComputerName = $task.Principal.PSComputerName + # RequiredPrivilege = $task.Principal.RequiredPrivilege + # UserId = $task.Principal.UserId + # } # Combine task properties with task info properties # Get-ScheduledTask -TaskName "XYZ" | Get-Member -MemberType Property @@ -129,15 +132,12 @@ foreach ($task in $tasks) { LastTaskResult = $taskInfo.LastTaskResult NextRunTime = $taskInfo.NextRunTime NumberOfMissedRuns = $taskInfo.NumberOfMissedRuns - UserId = $principal.UserId - Enabled = $settings.Enabled - Priority = $settings.Priority - Hidden = $settings.Hidden - ExecutionTimeLimit = $settings.ExecutionTimeLimit - Principal = $principal + UserId = $task.Principal.UserId + Enabled = $task.Settings.Enabled + Priority = $task.Settings.Priority + Hidden = $task.Settings.Hidden + ExecutionTimeLimit = $task.Settings.ExecutionTimeLimit Actions = $actions - Triggers = $triggers - Settings = $settings } } diff --git a/pkg/snclient/snclient_windows.go b/pkg/snclient/snclient_windows.go index 90d925ef..b2f5ee0c 100644 --- a/pkg/snclient/snclient_windows.go +++ b/pkg/snclient/snclient_windows.go @@ -402,10 +402,25 @@ type PowerShellParameter struct { specifiedValue string } -func powerShellCmd(ctx context.Context, command string, parameters ...PowerShellParameter) (cmd *exec.Cmd) { +func powerShellCmd(ctx context.Context, command string, parameters ...PowerShellParameter) (cmd *exec.Cmd, err error) { cmd = exec.CommandContext(ctx, "powershell") cmd.Args = nil + checkQuoutes := func(str string) bool { + if strings.ContainsRune(str, '\'') || strings.ContainsRune(str, '"') { + return true + } + + return false + } + for _, para := range parameters { + if checkQuoutes(para.name) || checkQuoutes(para.parameterType) || + checkQuoutes(para.defaultValue) || checkQuoutes(para.specifiedValue) { + return nil, errors.New("one of the parameters has its name/type or values contain single or double quoutes") + } + } + + // the template looks like this: // powershellInvocationArguments "& { param([string]$param1='defaultValue1', [string]$param2='defaultValue2') scriptContent }" -param1 "value1" -param2 "value2" parameterDefinitions := make([]string, 0, len(parameters)) @@ -434,7 +449,7 @@ func powerShellCmd(ctx context.Context, command string, parameters ...PowerShell CmdLine: cmdLine, } - return cmd + return cmd, nil } func isBatchFile(path string) bool { From 57e68bfcba53550eefa172f5f218a504fb5797ab Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 18 Jun 2026 15:31:21 +0200 Subject: [PATCH 14/14] check_tasksched: citest fixes --- docs/checks/commands/check_tasksched.md | 4 +++- pkg/snclient/check_tasksched_windows.go | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/checks/commands/check_tasksched.md b/docs/checks/commands/check_tasksched.md index 4776da3e..2594624d 100644 --- a/docs/checks/commands/check_tasksched.md +++ b/docs/checks/commands/check_tasksched.md @@ -87,4 +87,6 @@ these can be used in filters and thresholds (along with the default attributes): | missed_runs | Number of times the registered task has missed a scheduled run | | task_status | Task status as string | | next_run_time | Time when the registered task is next scheduled to run | -| parameters | Command line parameters for the task | +| parameters | Last actions command line parameters | +| execute | Last actions executed program | +| working_dir | Last actions working directory | diff --git a/pkg/snclient/check_tasksched_windows.go b/pkg/snclient/check_tasksched_windows.go index 1c0ab7ca..443bd02c 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -68,7 +68,6 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD specifiedValue: strconv.FormatBool(l.Recursive), }, ) - if err != nil { return fmt.Errorf("error when building a powershell command: %s", err.Error()) }