Skip to content
Merged
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
15 changes: 8 additions & 7 deletions pkg/snclient/check_tasksched_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"strings"
"syscall"
"time"
"unicode"

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

titleRuneBlacklist := []rune{'\\', '/', ':', '*', '?', '"', '<', '>', '|'}
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 strings.ContainsFunc(l.TaskTitle, func(r rune) bool { return slices.Contains(titleRuneBlacklist, r) }) {
return fmt.Errorf("custom specified title: '%s' contains one of the blacklisted runes: '%s' ", l.TaskTitle, string(titleRuneBlacklist))
}
}

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

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

for index := range taskList {
task := taskList[index]
Expand Down
195 changes: 93 additions & 102 deletions pkg/snclient/embed/scripts/windows/scheduled_tasks.ps1
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# list scheduled tasks in json format
# list scheduled tasks in json format
# this version uses the Schedule.Service COM API
# it avoids importing the ScheduledTasks module, which can be extremely slow
# on machines with EDR/antivirus solutions that scan modules via AMSI
# usage: .\scheduled_tasks.ps1 [-title <pattern>] [-folder <path>] [-recursive <true|false>]

# 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.
# When called via snclient, parameters are defined at the top of the script
# the parameters will be parsed without looking at $args
if ($args) {
for ($i = 0; $i -lt $args.Count; $i++) {
if ($args[$i] -eq '-title' -and $i + 1 -lt $args.Count) {
Expand All @@ -24,125 +27,113 @@ if ($args) {
}
}

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

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

$params = @{}
if ($title -ne '*') {
$params.TaskName = $title
}
if ($recursive -eq 'true') {
$params.TaskPath = $folder + '*'
} else {
$params.TaskPath = $folder
}
# 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 {
$tasks = Get-ScheduledTask @params -ErrorAction Stop
$targetFolder = $scheduler.GetFolder($folder)
$folderQueue = [System.Collections.Queue]::new()
$folderQueue.Enqueue($targetFolder)
while ($folderQueue.Count -gt 0) {
$currentFolder = $folderQueue.Dequeue()
# TASK_ENUM_HIDDEN = 1, include hidden tasks
# Call GetTasks() using TASK_ENUM_HIDDEN
foreach ($t in $currentFolder.GetTasks(1)) {
$tasks.Add($t)
}
if ($recursive -eq 'true') {
foreach ($sub in $currentFolder.GetFolders(0)) {
$folderQueue.Enqueue($sub)
}
}
}
} catch {
$tasks = @()
$tasks = [System.Collections.Generic.List[object]]::new()
}
$sw.Stop()
[Console]::Error.WriteLine(('Task enumeration took {0:F2} ms' -f $sw.Elapsed.TotalMilliseconds))

$results = @()
if ($title -ne '*') {
$filtered = [System.Collections.Generic.List[object]]::new()
foreach ($t in $tasks) {
if ($t.Name -eq $title) {
$filtered.Add($t)
}
}
$tasks = $filtered
}

$sw = [System.Diagnostics.Stopwatch]::StartNew()
$results = [System.Collections.Generic.List[object]]::new()
foreach ($task in $tasks) {
$taskInfo = Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath
$def = $task.Definition
$taskPath = $task.Path.Substring(0, $task.Path.Length - $task.Name.Length)

# Get-ScheduledTask returns a nested object
# Subobjects are not fully serialized and sent, only some of their fields are specifically selected
$actions = [System.Collections.Generic.List[object]]::new()
foreach ($action in $def.Actions) {
# COM IAction.Type: 0 = TASK_ACTION_EXEC (the only type with Path/Arguments/WorkingDirectory)
if ($action.Type -eq 0) {
$actions.Add(
[PSCustomObject]@{
Arguments = [string]$action.Arguments
Execute = [string]$action.Path
Id = [string]$action.Id
PSComputerName = ''
WorkingDirectory = [string]$action.WorkingDirectory
}
)
}
}

# 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 {
$results.Add(
[PSCustomObject]@{
Arguments = $_.Arguments
Execute = $_.Execute
Id = $_.Id
PSComputerName = $_.PSComputerName
WorkingDirectory = $_.WorkingDirectory
TaskName = $task.Name
TaskPath = $taskPath
State = [int]$task.State
Description = [string]$def.RegistrationInfo.Description
PSComputerName = ''
URI = $task.Path
Version = [string]$def.RegistrationInfo.Version
LastRunTime = $task.LastRunTime
LastTaskResult = [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$task.LastTaskResult), 0)
NextRunTime = $task.NextRunTime
NumberOfMissedRuns = [int64]$task.NumberOfMissedRuns
UserId = [string]$def.Principal.UserId
Enabled = [bool]$task.Enabled
Priority = [int64]$def.Settings.Priority
Hidden = [bool]$def.Settings.Hidden
ExecutionTimeLimit = [string]$def.Settings.ExecutionTimeLimit
Actions = @($actions)
}
})

# 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
# 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
}
)
}
$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
5 changes: 3 additions & 2 deletions pkg/snclient/snclient_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,9 @@ func powerShellCmd(ctx context.Context, command string, parameters ...PowerShell
cmdLine := fmt.Sprintf(`%s -Command "& { %s %s }" %s `, POWERSHELL, parameterDefinitionsCmdline, command, parameterSpecificationsCmdline)

cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CmdLine: cmdLine,
HideWindow: true,
CmdLine: cmdLine,
CreationFlags: windows.CREATE_NO_WINDOW,
}

return cmd, nil
Expand Down