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 = "Stop",

        [string[]]$Services = @(),

        [ValidateSet("Start", "Stop")]
        [string]$ServicesAction = "Stop",
        
        [string[]]$Commands = @(),

        [switch]$IncludeMirror
    )

    if (-not ($Processes.Count -or $Services.Count -or $Commands.Count)) {
        throw "You must specify at least one of -Processes, -Services, or -Commands."
    }

    $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($_)}
    }

    $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 1 WHERE TargetInstance ISA '$targetClass' AND ($targetsWQL)"
    } 
    else {
        $listenerQuery = "SELECT * FROM __InstanceDeletionEvent WITHIN 1 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 '$_'"
            })
        } 
        else {
            $procNames = Get-Basename($Processes)
            $cmdParts += ($procNames | ForEach-Object {
                "Stop-Process -Name '$_' -Force -ErrorAction SilentlyContinue"
            })
        }
    }

    # Commands
    if ($Commands) {
        $cmdParts += $Commands
    }
    
    $cmdString = $cmdParts -join "; "
    $action = "powershell.exe -Command `"$cmdString`""

    # Create WMI objects
    $instanceName = "$MonitorAction[" + ((Get-Basename $Targets) -join "-").Replace(' ', '') + "]"

    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 = $action
    }
    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 inversed 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\`"`""
            )

            foreach ($path in $wmiPaths) {
                try {
                    ([WMI]"\\.\root\subscription:$path").Delete()
                    Write-Verbose "Deleted: \\.\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)"
                    }
                }
            }
        }
    }
}