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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/checks/commands/check_tasksched.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 |
33 changes: 25 additions & 8 deletions pkg/snclient/check_tasksched.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@ 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 {
return &CheckTasksched{
TaskTitle: CheckTaskschedDefaultTaskTitle,
Folder: CheckTaskschedDefaultFolder,
Recursive: CheckTaskschedDefaultRecursive,
Hidden: CheckTaskschedDefaultHidden,
}
}

Expand All @@ -39,10 +42,24 @@ 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."},
"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",
Expand All @@ -56,24 +73,24 @@ 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},
{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
Expand Down
105 changes: 67 additions & 38 deletions pkg/snclient/check_tasksched_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
_ "embed"
"fmt"
"os/exec"
"slices"
"strconv"
"strings"
Expand All @@ -18,16 +19,13 @@ 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
// 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)/"

// 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
}
}
func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error {
l.cleanupArguments()

titleRuneBlacklist := []rune{'\\', '/', ':', '*', '?', '"', '<', '>', '|'}
if l.TaskTitle != CheckTaskschedDefaultTaskTitle {
Expand All @@ -44,33 +42,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),
},
)
cmd, err := l.buildPowershellCmd(ctx)
if err != nil {
return fmt.Errorf("error when building a powershell command: %s", err.Error())
}
Expand All @@ -93,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
}

Expand All @@ -118,7 +90,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)
}
Expand All @@ -131,6 +103,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 {
Expand Down
1 change: 1 addition & 0 deletions pkg/snclient/checkdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const (
UDate
UTimestamp
UPercent
UBool
)

type CheckAttribute struct {
Expand Down
24 changes: 23 additions & 1 deletion pkg/snclient/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`^([!=><~]+)(.*?)$`)
Expand Down Expand Up @@ -816,13 +820,29 @@ 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)
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 '<number> <unit>'
// 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)
Expand Down Expand Up @@ -870,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)
Expand Down
29 changes: 12 additions & 17 deletions pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,26 @@ if ($args) {
$i++
continue
}
if ($args[$i] -eq '-hidden' -and $i + 1 -lt $args.Count) {
$hidden = $args[$i + 1]
$i++
continue
}
}
}

# Apply defaults when variables are not defined (neither by snclient parameter injection nor by args)
if (!$title) { $title = '*' }
if (!$folder) { $folder = '\' }
if (!$recursive) { $recursive = 'true' }
if (!$hidden) { $hidden = 'false' }

# 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)
Expand All @@ -54,7 +53,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') {
Expand All @@ -66,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()
Expand All @@ -79,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
Expand Down Expand Up @@ -123,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
Loading
Loading