diff --git a/docs/checks/commands/check_tasksched.md b/docs/checks/commands/check_tasksched.md index ed98ac16..2594624d 100644 --- a/docs/checks/commands/check_tasksched.md +++ b/docs/checks/commands/check_tasksched.md @@ -41,22 +41,25 @@ 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 | 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 -| Argument | Description | -| -------- | ---------------------------------------------------------- | -| timezone | Sets the timezone for time metrics (default is local time) | +| 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 scheduled task, called TaskName in Powershell output. | ## Attributes @@ -64,22 +67,26 @@ 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 | Last actions command line parameters | +| execute | Last actions executed program | +| working_dir | Last actions working directory | 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 a3e23ef5..fd0beea8 100644 --- a/pkg/snclient/check_tasksched.go +++ b/pkg/snclient/check_tasksched.go @@ -10,10 +10,24 @@ func init() { AvailableChecks["check_tasksched"] = CheckEntry{"check_tasksched", NewCheckTasksched} } -type CheckTasksched struct{} +type CheckTasksched struct { + TaskTitle string + Folder string + Recursive bool +} + +const ( + CheckTaskschedDefaultTaskTitle string = "*" + CheckTaskschedDefaultFolder string = "\\" + CheckTaskschedDefaultRecursive bool = true +) func NewCheckTasksched() CheckHandler { - return &CheckTasksched{} + return &CheckTasksched{ + TaskTitle: CheckTaskschedDefaultTaskTitle, + Folder: CheckTaskschedDefaultFolder, + Recursive: CheckTaskschedDefaultRecursive, + } } func (l *CheckTasksched) Build() *CheckData { @@ -25,16 +39,19 @@ func (l *CheckTasksched) Build() *CheckData { State: CheckExitOK, }, args: map[string]CheckArgument{ - "timezone": {description: "Sets the timezone for time metrics (default is local time)"}, + "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."}, }, defaultFilter: "enabled = true", defaultCritical: "exit_code < 0", defaultWarning: "exit_code != 0", - detailSyntax: "${folder}/${title}: ${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"}, @@ -43,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}, @@ -52,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 3f22e4e1..443bd02c 100644 --- a/pkg/snclient/check_tasksched_windows.go +++ b/pkg/snclient/check_tasksched_windows.go @@ -7,8 +7,10 @@ import ( _ "embed" "fmt" "strconv" + "strings" "syscall" "time" + "unicode" "github.com/goccy/go-json" ) @@ -16,14 +18,66 @@ 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 { - cmd := powerShellCmd(ctx, scheduledTasksPS1) + 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 + } + } + + 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", + 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), + }, + ) + 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: %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, output: %s\n%s", exitCode, output, stderr) } var taskList []ScheduledTask @@ -38,33 +92,51 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD if task.LastRunTime != "" { hasRun = true } - parameters := "" 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, + "uri": task.URI, + "uri_clean": parseURIClean(task.URI), "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, + "parameters": l.parseParameters(task.Actions), + "execute": l.parseExecuteCmd(task.Actions), + "working_dir": l.parseWorkingDir(task.Actions), } 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(" { @@ -77,6 +149,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 @@ -192,24 +288,87 @@ func (t TaskState) String() string { } } +// The script does not export everything it discovers to JSON for snclient to parse +// When needed, modify the script and uncomment these lines + 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"` + Actions []ScheduledTaskAction `json:"Actions"` + // Principal ScheduledTaskPrincipal `json:"Principal"` + // Triggers []ScheduledTaskTrigger `json:"Triggers"` + // Settings ScheduledTaskSetting `json:"Settings"` } 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"` } + +// 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 61c3b2d2..ee9f5549 100644 --- a/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 +++ b/pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1 @@ -1,37 +1,148 @@ # list scheduled tasks in json format -# usage: .\scheduled_tasks.ps1 -# +# usage: .\scheduled_tasks.ps1 [-title ] [-folder ] [-recursive ] + +# 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 -Get-ScheduledTask | ForEach-Object { - $task = $_ +$params = @{} +if ($title -ne '*') { + $params.TaskName = $title +} +if ($recursive -eq 'true') { + $params.TaskPath = $folder + '*' +} else { + $params.TaskPath = $folder +} + +try { + $tasks = Get-ScheduledTask @params -ErrorAction Stop +} catch { + $tasks = @() +} + +$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]@{ - 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 = $task.Principal.UserId + Enabled = $task.Settings.Enabled + Priority = $task.Settings.Priority + Hidden = $task.Settings.Hidden + ExecutionTimeLimit = $task.Settings.ExecutionTimeLimit + Actions = $actions } -} | ConvertTo-Json -Depth 4 +} + +if ($results.Count -gt 0) { + ConvertTo-Json -InputObject $results -Depth 4 +} else { + '[]' +} diff --git a/pkg/snclient/snclient_windows.go b/pkg/snclient/snclient_windows.go index 1819a7f5..b2f5ee0c 100644 --- a/pkg/snclient/snclient_windows.go +++ b/pkg/snclient/snclient_windows.go @@ -393,15 +393,63 @@ 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, 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)) + 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, ", ")) + + 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, " ") + + cmdLine := fmt.Sprintf(`%s -Command "& { %s %s }" %s `, POWERSHELL, parameterDefinitionsCmdline, command, parameterSpecificationsCmdline) + 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 + return cmd, nil } func isBatchFile(path string) bool {