Skip to content

Commit 97d7087

Browse files
authored
Check tasksched fixes misc (#393)
1 parent a2d1265 commit 97d7087

3 files changed

Lines changed: 104 additions & 111 deletions

File tree

pkg/snclient/check_tasksched_windows.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"strings"
1212
"syscall"
1313
"time"
14-
"unicode"
1514

1615
"github.com/goccy/go-json"
1716
)
@@ -30,17 +29,18 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD
3029
}
3130
}
3231

32+
titleRuneBlacklist := []rune{'\\', '/', ':', '*', '?', '"', '<', '>', '|'}
3333
if l.TaskTitle != CheckTaskschedDefaultTaskTitle {
34-
if strings.ContainsFunc(l.TaskTitle, func(r rune) bool { return !unicode.IsLetter(r) }) {
35-
return fmt.Errorf("custom specified title should be all letters, but it isnt: %s", l.TaskTitle)
34+
if strings.ContainsFunc(l.TaskTitle, func(r rune) bool { return slices.Contains(titleRuneBlacklist, r) }) {
35+
return fmt.Errorf("custom specified title: '%s' contains one of the blacklisted runes: '%s' ", l.TaskTitle, string(titleRuneBlacklist))
3636
}
3737
}
3838

39+
// allow backslashes when specifying folders, to specify nested paths
40+
folderRuneBlacklist := []rune{'/', ':', '*', '?', '"', '<', '>', '|'}
3941
if l.Folder != CheckTaskschedDefaultFolder {
40-
// NTFS characters are generally allowed, expect quotes
41-
allowedRunes := []rune{' ', '-', '\\', '_', '(', ')', '[', ']', '.', ','}
42-
if strings.ContainsFunc(l.Folder, func(r rune) bool { return !unicode.IsLetter(r) && !slices.Contains(allowedRunes, r) }) {
43-
return fmt.Errorf("custom specified folder should be all letters or allowed runes: '%s', but it isnt: %s", string(allowedRunes), l.Folder)
42+
if strings.ContainsFunc(l.Folder, func(r rune) bool { return slices.Contains(folderRuneBlacklist, r) }) {
43+
return fmt.Errorf("custom specified folder: '%s' contains one of the blacklisted runes: '%s' ", l.Folder, string(folderRuneBlacklist))
4444
}
4545
}
4646

@@ -88,6 +88,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD
8888
if err != nil {
8989
return fmt.Errorf("could not unmarshal scheduled tasks: %s", err.Error())
9090
}
91+
log.Debugf("found %d scheduled task(s)", len(taskList))
9192

9293
for index := range taskList {
9394
task := taskList[index]
Lines changed: 93 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
# list scheduled tasks in json format
1+
# list scheduled tasks in json format
2+
# this version uses the Schedule.Service COM API
3+
# it avoids importing the ScheduledTasks module, which can be extremely slow
4+
# on machines with EDR/antivirus solutions that scan modules via AMSI
25
# usage: .\scheduled_tasks.ps1 [-title <pattern>] [-folder <path>] [-recursive <true|false>]
36

47
# Parse named arguments (for standalone invocation).
5-
# When called via snclient, variables are injected at the top of the script instead,
6-
# so $args will be empty and this loop does nothing.
8+
# When called via snclient, parameters are defined at the top of the script
9+
# the parameters will be parsed without looking at $args
710
if ($args) {
811
for ($i = 0; $i -lt $args.Count; $i++) {
912
if ($args[$i] -eq '-title' -and $i + 1 -lt $args.Count) {
@@ -24,125 +27,113 @@ if ($args) {
2427
}
2528
}
2629

27-
# Apply defaults when variables are not defined (neither by snclient injection nor by args)
30+
# Apply defaults when variables are not defined (neither by snclient parameter injection nor by args)
2831
if (!$title) { $title = '*' }
2932
if (!$folder) { $folder = '\' }
30-
if (!$recursive) { $recursive = 'false' }
33+
if (!$recursive) { $recursive = 'true' }
3134

3235
# ensure output is utf8
3336
$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8
3437

35-
$params = @{}
36-
if ($title -ne '*') {
37-
$params.TaskName = $title
38-
}
39-
if ($recursive -eq 'true') {
40-
$params.TaskPath = $folder + '*'
41-
} else {
42-
$params.TaskPath = $folder
43-
}
38+
# Print powershell version
39+
[Console]::Error.WriteLine(('Powershell version table: ' + ($PSVersionTable | ConvertTo-Json -Compress)))
40+
41+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
42+
$scheduler = New-Object -ComObject Schedule.Service
43+
$scheduler.Connect()
44+
$sw.Stop()
45+
[Console]::Error.WriteLine(('COM Schedule.Service connect took {0:F2} ms' -f $sw.Elapsed.TotalMilliseconds))
4446

47+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
48+
$tasks = [System.Collections.Generic.List[object]]::new()
4549
try {
46-
$tasks = Get-ScheduledTask @params -ErrorAction Stop
50+
$targetFolder = $scheduler.GetFolder($folder)
51+
$folderQueue = [System.Collections.Queue]::new()
52+
$folderQueue.Enqueue($targetFolder)
53+
while ($folderQueue.Count -gt 0) {
54+
$currentFolder = $folderQueue.Dequeue()
55+
# TASK_ENUM_HIDDEN = 1, include hidden tasks
56+
# Call GetTasks() using TASK_ENUM_HIDDEN
57+
foreach ($t in $currentFolder.GetTasks(1)) {
58+
$tasks.Add($t)
59+
}
60+
if ($recursive -eq 'true') {
61+
foreach ($sub in $currentFolder.GetFolders(0)) {
62+
$folderQueue.Enqueue($sub)
63+
}
64+
}
65+
}
4766
} catch {
48-
$tasks = @()
67+
$tasks = [System.Collections.Generic.List[object]]::new()
4968
}
69+
$sw.Stop()
70+
[Console]::Error.WriteLine(('Task enumeration took {0:F2} ms' -f $sw.Elapsed.TotalMilliseconds))
5071

51-
$results = @()
72+
if ($title -ne '*') {
73+
$filtered = [System.Collections.Generic.List[object]]::new()
74+
foreach ($t in $tasks) {
75+
if ($t.Name -eq $title) {
76+
$filtered.Add($t)
77+
}
78+
}
79+
$tasks = $filtered
80+
}
81+
82+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
83+
$results = [System.Collections.Generic.List[object]]::new()
5284
foreach ($task in $tasks) {
53-
$taskInfo = Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath
85+
$def = $task.Definition
86+
$taskPath = $task.Path.Substring(0, $task.Path.Length - $task.Name.Length)
5487

55-
# Get-ScheduledTask returns a nested object
56-
# Subobjects are not fully serialized and sent, only some of their fields are specifically selected
88+
$actions = [System.Collections.Generic.List[object]]::new()
89+
foreach ($action in $def.Actions) {
90+
# COM IAction.Type: 0 = TASK_ACTION_EXEC (the only type with Path/Arguments/WorkingDirectory)
91+
if ($action.Type -eq 0) {
92+
$actions.Add(
93+
[PSCustomObject]@{
94+
Arguments = [string]$action.Arguments
95+
Execute = [string]$action.Path
96+
Id = [string]$action.Id
97+
PSComputerName = ''
98+
WorkingDirectory = [string]$action.WorkingDirectory
99+
}
100+
)
101+
}
102+
}
57103

58-
# Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Actions | Get-Member -MemberType Property
59-
# 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
60-
$actions = @($task.Actions | ForEach-Object {
104+
$results.Add(
61105
[PSCustomObject]@{
62-
Arguments = $_.Arguments
63-
Execute = $_.Execute
64-
Id = $_.Id
65-
PSComputerName = $_.PSComputerName
66-
WorkingDirectory = $_.WorkingDirectory
106+
TaskName = $task.Name
107+
TaskPath = $taskPath
108+
State = [int]$task.State
109+
Description = [string]$def.RegistrationInfo.Description
110+
PSComputerName = ''
111+
URI = $task.Path
112+
Version = [string]$def.RegistrationInfo.Version
113+
LastRunTime = $task.LastRunTime
114+
LastTaskResult = [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$task.LastTaskResult), 0)
115+
NextRunTime = $task.NextRunTime
116+
NumberOfMissedRuns = [int64]$task.NumberOfMissedRuns
117+
UserId = [string]$def.Principal.UserId
118+
Enabled = [bool]$task.Enabled
119+
Priority = [int64]$def.Settings.Priority
120+
Hidden = [bool]$def.Settings.Hidden
121+
ExecutionTimeLimit = [string]$def.Settings.ExecutionTimeLimit
122+
Actions = @($actions)
67123
}
68-
})
69-
70-
# Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Triggers | Get-Member -MemberType Property
71-
# $triggers = @($task.Triggers | ForEach-Object {
72-
# [PSCustomObject]@{
73-
# DaysInterval = $_.DaysInterval
74-
# Enabled = $_.Enabled
75-
# EndBoundary = $_.EndBoundary
76-
# ExecutionTimeLimit = $_.ExecutionTimeLimit
77-
# Id = $_.Id
78-
# RandomDelay = $_.RandomDelay
79-
# Repetition = $_.Repetition
80-
# StartBoundary = $_.StartBoundary
81-
# }
82-
# })
83-
84-
# Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Settings | Get-Member -MemberType Property
85-
# $settings = [PSCustomObject]@{
86-
# AllowDemandStart = $task.Settings.AllowDemandStart
87-
# AllowHardTerminate = $task.Settings.AllowHardTerminate
88-
# DeleteExpiredTaskAfter = $task.Settings.DeleteExpiredTaskAfter
89-
# DisallowStartIfOnBatteries = $task.Settings.DisallowStartIfOnBatteries
90-
# DisallowStartOnRemoteAppSession = $task.Settings.DisallowStartOnRemoteAppSession
91-
# Enabled = $task.Settings.Enabled
92-
# ExecutionTimeLimit = $task.Settings.ExecutionTimeLimit
93-
# Hidden = $task.Settings.Hidden
94-
# IdleSettings = $task.Settings.IdleSettings
95-
# MaintenanceSettings = $task.Settings.MaintenanceSettings
96-
# NetworkSettings = $task.Settings.NetworkSettings
97-
# Priority = $task.Settings.Priority
98-
# PSComputerName = $task.Settings.PSComputerName
99-
# RestartCount = $task.Settings.RestartCount
100-
# RestartInterval = $task.Settings.RestartInterval
101-
# RunOnlyIfIdle = $task.Settings.RunOnlyIfIdle
102-
# RunOnlyIfNetworkAvailable = $task.Settings.RunOnlyIfNetworkAvailable
103-
# StartWhenAvailable = $task.Settings.StartWhenAvailable
104-
# StopIfGoingOnBatteries = $task.Settings.StopIfGoingOnBatteries
105-
# UseUnifiedSchedulingEngine = $task.Settings.UseUnifiedSchedulingEngine
106-
# Volatile = $task.Settings.Volatile
107-
# WakeToRun = $task.Settings.WakeToRun
108-
# }
109-
110-
# Get-ScheduledTask -TaskName "XYZ" | Select-Object -ExpandProperty Principal | Get-Member -MemberType Property
111-
# $principal = [PSCustomObject]@{
112-
# DisplayName = $task.Principal.DisplayName
113-
# Id = $task.Principal.Id
114-
# GroupId = $task.Principal.GroupId
115-
# PSComputerName = $task.Principal.PSComputerName
116-
# RequiredPrivilege = $task.Principal.RequiredPrivilege
117-
# UserId = $task.Principal.UserId
118-
# }
119-
120-
# Combine task properties with task info properties
121-
# Get-ScheduledTask -TaskName "XYZ" | Get-Member -MemberType Property
122-
# Get-ScheduledTaskInfo -TaskName "XYZ" | Get-Member -MemberType Property
123-
$results += [PSCustomObject]@{
124-
TaskName = $task.TaskName
125-
TaskPath = $task.TaskPath
126-
State = $task.State
127-
Description = $task.Description
128-
PSComputerName = $task.PSComputerName
129-
URI = $task.URI
130-
Version = $task.Version
131-
LastRunTime = $taskInfo.LastRunTime
132-
LastTaskResult = $taskInfo.LastTaskResult
133-
NextRunTime = $taskInfo.NextRunTime
134-
NumberOfMissedRuns = $taskInfo.NumberOfMissedRuns
135-
UserId = $task.Principal.UserId
136-
Enabled = $task.Settings.Enabled
137-
Priority = $task.Settings.Priority
138-
Hidden = $task.Settings.Hidden
139-
ExecutionTimeLimit = $task.Settings.ExecutionTimeLimit
140-
Actions = $actions
141-
}
124+
)
142125
}
126+
$sw.Stop()
127+
[Console]::Error.WriteLine(('Populating results list took {0:F2} ms' -f $sw.Elapsed.TotalMilliseconds))
128+
[Console]::Error.WriteLine(('Results list has {0} elements' -f $results.Count))
143129

130+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
144131
if ($results.Count -gt 0) {
145132
ConvertTo-Json -InputObject $results -Depth 4
146133
} else {
147134
'[]'
148135
}
136+
$sw.Stop()
137+
[Console]::Error.WriteLine(('Converting to JSON took {0:F2} ms' -f $sw.Elapsed.TotalMilliseconds))
138+
139+
exit 0

pkg/snclient/snclient_windows.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,9 @@ func powerShellCmd(ctx context.Context, command string, parameters ...PowerShell
445445
cmdLine := fmt.Sprintf(`%s -Command "& { %s %s }" %s `, POWERSHELL, parameterDefinitionsCmdline, command, parameterSpecificationsCmdline)
446446

447447
cmd.SysProcAttr = &syscall.SysProcAttr{
448-
HideWindow: true,
449-
CmdLine: cmdLine,
448+
HideWindow: true,
449+
CmdLine: cmdLine,
450+
CreationFlags: windows.CREATE_NO_WINDOW,
450451
}
451452

452453
return cmd, nil

0 commit comments

Comments
 (0)