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
710if ($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)
2831if (! $title ) { $title = ' *' }
2932if (! $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()
4549try {
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()
5284foreach ($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()
144131if ($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
0 commit comments