WmiListener.psm1
<#
WmiListener - Process/Service orchestration via WMI event subscriptions Author: Joel055 License: MIT https://github.com/Joel055/WmiListener #> if ([System.Environment]::OSVersion.Platform -ne "Win32NT") { throw "WmiListener requires Windows. WMI permanent event subscriptions are only supported on Windows platforms." } if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(544)) { throw "WmiListener requires elevation. Run PowerShell as Administrator to import the module." } function Register-Listener { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string[]]$Targets, [switch]$TargetIsService, [Parameter(Mandatory=$true)] [ValidateSet("Start", "Stop")] [string]$MonitorAction, [string[]]$Processes = @(), [ValidateSet("Start", "Stop")] [string]$ProcessesAction, [string[]]$Services = @(), [ValidateSet("Start", "Stop")] [string]$ServicesAction, [string[]]$Commands = @(), [string]$User = "$env:USERDOMAIN\$env:USERNAME", [switch]$IncludeMirror ) # Validate action parameters if (-not ($Processes.Count -or $Services.Count -or $Commands.Count)) { throw "You must specify at least one of -Processes, -Services, or -Commands." } if ($Processes.Count -gt 0 -and -not $PSBoundParameters.ContainsKey('ProcessesAction')) { throw "-ProcessesAction is required when -Processes is specified." } if ($Services.Count -gt 0 -and -not $PSBoundParameters.ContainsKey('ServicesAction')) { throw "-ServicesAction is required when -Services is specified." } # Warn about mirror behaviour if ($Commands -and $IncludeMirror) { Write-Warning "Arguments passed to -Commands cannot be automatically inverted and will be executed as-is in the mirrored listener." } $Targets = $Targets | ForEach-Object {$_.ToLower()} $MonitorAction = $MonitorAction.Substring(0,1).ToUpper() + $MonitorAction.Substring(1).ToLower() function Get-Basename { param ([array]$Path) $Path | ForEach-Object {[System.IO.Path]::GetFileNameWithoutExtension($_)} } function New-UserContextScheduledTask { param ( [string]$TaskName, [string]$ActionCommand, [string]$RunAs ) $service = New-Object -ComObject "Schedule.Service" $service.Connect() $root = $service.GetFolder("\") $task = $service.NewTask(0) $task.RegistrationInfo.Description = "Execute user-defined actions in response to WMI events." $task.RegistrationInfo.Author = "WmiListener" $task.Principal.UserId = "$RunAs" $task.Principal.LogonType = 3 $task.Principal.RunLevel = 1 $action = $task.Actions.Create(0) $action.Path = "conhost.exe" $action.Arguments = "--headless -- powershell.exe -NoProfile -ExecutionPolicy Bypass -Command `"$ActionCommand`"" $root.RegisterTaskDefinition($TaskName, $task, 6, $null, $null, 3) | Out-Null } $targetsWQL = ($Targets | ForEach-Object {"(TargetInstance.Name='$_')"}) -join " OR " Write-Verbose "Targets: $Targets" Write-Verbose "targetsWQL: $targetsWQL" if ($TargetIsService.IsPresent) { $targetClass = 'Win32_Service' } else { $targetClass = 'Win32_Process' } if ($MonitorAction -eq "Start") { $listenerQuery = "SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance ISA '$targetClass' AND ($targetsWQL)" } else { $listenerQuery = "SELECT * FROM __InstanceDeletionEvent WITHIN 5 WHERE TargetInstance ISA '$targetClass' AND ($targetsWQL)" } Write-Verbose "WQL ListenerQuery: $listenerQuery" # Build a list of actions $cmdParts = @() # Services if ($Services) { $cmdParts += ($Services | ForEach-Object { if ($ServicesAction -eq "Start") { "Start-Service -Name '$_' -ErrorAction SilentlyContinue" } else { "Stop-Service -Name '$_' -ErrorAction SilentlyContinue" } }) } # Processes if ($Processes) { if ($ProcessesAction -eq "Start") { $cmdParts += ($Processes | ForEach-Object { "Start-Process -FilePath '$_' -ErrorAction SilentlyContinue" }) } else { $procNames = Get-Basename($Processes) $cmdParts += ($procNames | ForEach-Object { "Stop-Process -Name '$_' -Force -ErrorAction SilentlyContinue" }) } } # Commands if ($Commands) { $cmdParts += $Commands } # Create command-string $cmdString = $cmdParts -join "; " $instanceName = "$MonitorAction[" + ((Get-Basename $Targets) -join "-").Replace(' ', '') + "]" Write-Verbose "CommandString: $cmdString" Write-Verbose "InstanceStr: $instanceName" New-UserContextScheduledTask -TaskName $instanceName -ActionCommand $cmdString -RunAs $User # Create WMI objects if ($instanceName -in (Get-Listener)) { throw "Listener-instance `"$instanceName`" already exists." } $filter = Set-WmiInstance -Namespace root\subscription -Class __EventFilter -Arguments @{ Name = "${instanceName}:Listener" EventNamespace = "root\cimv2" QueryLanguage = "WQL" Query = $listenerQuery } Write-Verbose "Filter created." $consumer = Set-WmiInstance -Namespace root\subscription -Class CommandLineEventConsumer -Arguments @{ Name = "${instanceName}:Consumer" CommandLineTemplate = "schtasks /Run /TN $instanceName" } Write-Verbose "Consumer created." $null = Set-WmiInstance -Namespace root\subscription -Class __FilterToConsumerBinding -Arguments @{ Filter = $filter.__RELPATH Consumer = $consumer.__RELPATH } Write-Verbose "Binding created." Write-Host "Created listener: " -ForegroundColor Green -NoNewline Write-Host "$instanceName" -ForegroundColor Yellow # Create inverted listener if ($IncludeMirror) { if ($ProcessesAction -eq "Stop") { $hasOnlyNames = $Processes | Where-Object { -not ($_ -like "*\*") } if ($hasOnlyNames) { $namesStr = ($hasOnlyNames -join '", "') $mirrorWarning = "Mirror listener may fail to start process(es) [`"$namesStr`"] because only the executable name(s) was provided. Consider using full paths." } } $inverseParam = $PSBoundParameters $inverseParam["includeMirror"] = $false # Flip the action of each parameter foreach ($key in $inverseParam.Keys) { if ($inverseParam[$key] -eq "Start") { $inverseParam[$key] = "Stop" } elseif ($inverseParam[$key] -eq "Stop") { $inverseParam[$key] = "Start" } } & $MyInvocation.MyCommand.Name @inverseParam if ($mirrorWarning) { Write-Warning $mirrorWarning } } Write-Host "" } function Get-Listener { [CmdletBinding()] [OutputType([String])] param ( [Parameter(ValueFromPipeline=$true, Position=0)] [ValidatePattern("Listener$")] [string]$Listener ) begin { # Look for __EventFilter objects with the suffix "Listener" that were created by the current user $filter = @() $userSID = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value $allObjects = Get-WmiObject -Namespace root\subscription -Class __EventFilter | Where-Object {$_.Name -like "*Listener"} | Where-Object {(New-Object System.Security.Principal.SecurityIdentifier($_.CreatorSID, 0)).Value -eq $userSID} } process { if ($Listener) {$filter += $Listener} } end { if ($filter) { $validObjects = $allObjects | Where-Object {$_.Name -in $filter} } else { $validObjects = $allObjects } if ($validObjects) { foreach ($obj in $validObjects) { Write-Output(($obj.Name -split ':')[0]) } } else { Write-Verbose "No user-defined listeners found." } } } function Remove-Listener { [CmdletBinding()] param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)] [ValidatePattern("^\S.*")] [string]$Listener ) begin {$allListeners = @()} process {$allListeners += $Listener} end { foreach ($listener in $allListeners) { # Fetches associated WMI objects based on the listenernames provided $wmiPaths = @( "CommandLineEventConsumer.Name='${listener}:Consumer'", "__EventFilter.Name='${listener}:Listener'", "__FilterToConsumerBinding.Consumer=`"CommandLineEventConsumer.Name=\`"${listener}:Consumer\`"`",Filter=`"__EventFilter.Name=\`"${listener}:Listener\`"`"" ) Write-Verbose "LISTENER $listener" foreach ($path in $wmiPaths) { try { ([WMI]"\\.\root\subscription:$path").Delete() Write-Verbose "Removed \\.\root\subscription:$path" } catch { if ($_.Exception.Message -like "*Not found*") { Write-Verbose "Object $path not found for Listener `"$listener`", skipping." } else { Write-Warning "Error removing `"$listener`": $($_.Exception.Message)" } } } $taskName = ($listener -split ':')[0] $service = New-Object -ComObject "Schedule.Service" $service.Connect() $root = $service.GetFolder("\") try { $root.DeleteTask($taskName, 0) Write-Verbose "Scheduled task '$taskName' removed." } catch { if ($_.Exception.Message -like "*0x80070002*") { Write-Verbose "Scheduled task '$taskName' not found. Skipping." } else { Write-Warning "Error removing scheduled task '$taskName': $($_.Exception.Message)" } } } } } Write-Host "WmiListener loaded. Run `"Get-Help Register-Listener -Full`" for usage instructions." -ForegroundColor Cyan |