Guide Programming + Scripting Powershell ProcWatcher - Powershell script to see see and monitor all programs running on a computer. Updated March 31 2026
This script will let you see all processes that start on your computer, logging the details of each to a log file.

If you save it to a file, you can call it with a batch file or shortcut like this, letting you have it ignore certain processes:
Copy to ClipboardPowerShell.exe -ExecutionPolicy Bypass -File C:\Tech208\Utilities\Scripts\Powershell\ProcWatcher.ps1 -WhiteList "chrome.exe, conhost.exe, ctfmon.exe, msedge.exe, firefox.exe, WmiPrvSE.exe, SearchProtocolHost.exe, DataExchangeHost.exe" -LogPath "%USERPROFILE%"



Powershell Script Below:
Copy to Clipboard <# .SYNOPSIS Monitors and displays the full path of every new process started on the system. .DESCRIPTION Uses a WMI event subscription (Win32_ProcessStartTrace) to hook into process creation events in real time, catching even short-lived processes that would be missed by polling. Supports path/partial-path whitelisting, optional date-stamped log files, and an optional hidden-window mode. .PARAMETER LogPath Directory to write log files to. A file named ProcessLog_yyyy-MM-dd.log will be created (or appended to) in this directory. Defaults to C:\Users\\ProcWatcher. .PARAMETER WhitelistFile Path to a text file containing one whitelist entry per line. Any process whose full path contains a listed string (case-insensitive) is silently skipped. Lines starting with # and blank lines are ignored. .PARAMETER Whitelist A comma-separated string of partial paths or filenames to whitelist, as an alternative (or addition) to WhitelistFile. Example: -Whitelist "svchost.exe, conhost.exe, C:\Windows\SysWOW64" .PARAMETER Hidden If specified, the script re-launches itself in a hidden PowerShell window and exits the current console. Requires -LogPath so output is not lost. .PARAMETER PollIntervalMs How often (in milliseconds) to check for queued events. Lower values are more responsive but use more CPU. Default: 250. .EXAMPLE .\Watch-Processes.ps1 # Interactive monitor — prints every new process to the console. .EXAMPLE .\Watch-Processes.ps1 -LogPath C:\Logs -WhitelistFile C:\Logs\whitelist.txt # Monitors, logs to C:\Logs\ProcessLog_2026-03-30.log, skips whitelisted paths. .EXAMPLE .\Watch-Processes.ps1 -LogPath C:\Logs -Hidden # Launches a hidden background window that logs silently. .NOTES Requires elevation (Run as Administrator) to receive WMI process-start events. Press Ctrl+C to stop when running interactively. #> [CmdletBinding()] param( [string]$LogPath = (Join-Path $env:USERPROFILE 'ProcWatcher'), [string]$WhitelistFile, [string]$Whitelist = '', [switch]$Hidden, [int]$PollIntervalMs = 250 ) # ── Re-launch hidden if requested ──────────────────────────────────────────── if ($Hidden) { # Rebuild argument list without -Hidden to avoid infinite recursion $argList = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$PSCommandPath`"") $argList += "-LogPath", "`"$LogPath`"" if ($WhitelistFile) { $argList += "-WhitelistFile", "`"$WhitelistFile`"" } if ($Whitelist) { $argList += "-Whitelist", "`"$Whitelist`"" } if ($PollIntervalMs -ne 250) { $argList += "-PollIntervalMs", $PollIntervalMs } Start-Process powershell.exe -ArgumentList $argList -WindowStyle Hidden Write-Host "Launched hidden process monitor. Logs will appear in: $LogPath" -ForegroundColor Green return } # ── Elevation check ────────────────────────────────────────────────────────── $isAdmin = ([Security.Principal.WindowsPrincipal] ` [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { Write-Warning "Not running as Administrator. WMI process events may not be received." Write-Warning "Consider re-running from an elevated prompt." } # ── Build whitelist ────────────────────────────────────────────────────────── $whitelistEntries = [System.Collections.Generic.List[string]]::new() # Inline entries (comma-separated string) foreach ($entry in ($Whitelist -split ',')) { $trimmed = $entry.Trim() if ($trimmed -and -not $trimmed.StartsWith('#')) { $whitelistEntries.Add($trimmed) } } # File entries if ($WhitelistFile -and (Test-Path $WhitelistFile)) { foreach ($line in (Get-Content $WhitelistFile -ErrorAction SilentlyContinue)) { $trimmed = $line.Trim() if ($trimmed -and -not $trimmed.StartsWith('#')) { $whitelistEntries.Add($trimmed) } } Write-Host "Loaded $($whitelistEntries.Count) whitelist entries." -ForegroundColor Cyan }elseif ($WhitelistFile) { Write-Warning "Whitelist file not found: $WhitelistFile — continuing without it." } # ── Logging helper ─────────────────────────────────────────────────────────── $script:LogFile = $null function Initialize-LogFile { if (-not $LogPath) { return } if (-not (Test-Path $LogPath)) { New-Item -Path $LogPath -ItemType Directory -Force | Out-Null } $datestamp = (Get-Date).ToString('yyyy-MM-dd') $script:LogFile = Join-Path $LogPath "ProcessLog_$datestamp.log" } function Write-Log { param([string]$Message) # Roll over to a new file at midnight if ($script:LogFile) { $expectedName = "ProcessLog_$((Get-Date).ToString('yyyy-MM-dd')).log" if ((Split-Path $script:LogFile -Leaf) -ne $expectedName) { Initialize-LogFile } } if ($script:LogFile) { $Message | Out-File -FilePath $script:LogFile -Append -Encoding utf8 } } Initialize-LogFile # ── P/Invoke for fast path resolution ──────────────────────────────────────── Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; using System.Text; public class NativeProcessHelper { [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr OpenProcess(uint access, bool inherit, uint pid); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr handle); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool QueryFullProcessImageNameW( IntPtr hProcess, uint flags, StringBuilder name, ref uint size); public static string GetImagePath(uint pid) { // PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 IntPtr handle = OpenProcess(0x1000, false, pid); if (handle == IntPtr.Zero) return null; try { StringBuilder sb = new StringBuilder(1024); uint size = 1024; if (QueryFullProcessImageNameW(handle, 0, sb, ref size)) return sb.ToString(); return null; } finally { CloseHandle(handle); } } } "@ -ErrorAction Stop # ── Resolve process path helper ────────────────────────────────────────────── function Get-ProcessPath { param([uint32]$ProcessId) # 1) P/Invoke — fastest, goes straight to the kernel handle try { $path = [NativeProcessHelper]::GetImagePath($ProcessId) if ($path) { return $path } } catch { } # 2) .NET Process object — slightly slower but still quick try { $proc = Get-Process -Id $ProcessId -ErrorAction Stop if ($proc.Path) { return $proc.Path } } catch { } # 3) WMI — slowest fallback try { $wmiProc = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction Stop if ($wmiProc.ExecutablePath) { return $wmiProc.ExecutablePath } } catch { } return $null } # ── Resolve process command line ───────────────────────────────────────────── function Get-ProcessCommandLine { param([uint32]$ProcessId) try { $wmiProc = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction Stop if ($wmiProc.CommandLine) { return $wmiProc.CommandLine } } catch { } return $null } # ── Test whitelist ─────────────────────────────────────────────────────────── function Test-Whitelisted { param([string]$Path) if (-not $Path -or $whitelistEntries.Count -eq 0) { return $false } foreach ($entry in $whitelistEntries) { if ($Path.IndexOf($entry, [StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true } } return $false } # ── Register WMI events (start + stop) ─────────────────────────────────────── $startSourceId = "WatchProcessStart_$PID" $stopSourceId = "WatchProcessStop_$PID" try { Register-WmiEvent -Query "SELECT * FROM Win32_ProcessStartTrace" ` -SourceIdentifier $startSourceId -ErrorAction Stop | Out-Null Register-WmiEvent -Query "SELECT * FROM Win32_ProcessStopTrace" ` -SourceIdentifier $stopSourceId -ErrorAction Stop | Out-Null }catch { Write-Error "Failed to register WMI event subscription: $_" Write-Error "Make sure you are running as Administrator." # Clean up whichever succeeded Unregister-Event -SourceIdentifier $startSourceId -ErrorAction SilentlyContinue Unregister-Event -SourceIdentifier $stopSourceId -ErrorAction SilentlyContinue return } # ── Tracking state ─────────────────────────────────────────────────────────── # Maps PID -> path so we can display the path on exit (process is already gone) $script:pidPathMap = @{} # Maps PID -> command line for exit display $script:pidCmdMap = @{} # Maps PID -> [datetime] so we can calculate run duration on exit $script:pidStartMap = @{} # Tracks EventIdentifiers we've already processed to prevent duplicates $script:seenEvents = [System.Collections.Generic.HashSet[int]]::new() # ── Banner ─────────────────────────────────────────────────────────────────── $banner = @" ====================================================== Process Monitor — $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Logging : $(if ($script:LogFile) { $script:LogFile } else { 'Disabled' }) Whitelist : $($whitelistEntries.Count) entries Press Ctrl+C to stop. ====================================================== "@ Write-Host $banner -ForegroundColor Yellow Write-Log $banner # ── Main loop ──────────────────────────────────────────────────────────────── try { while ($true) { # Wait briefly for any event from either source $event = $null $event = Wait-Event -Timeout ([math]::Max(1, [int]($PollIntervalMs / 1000.0 + 0.5))) -ErrorAction SilentlyContinue if (-not $event) { continue } # Drain all queued events from both sources $events = @($event) $events += @(Get-Event -SourceIdentifier $startSourceId -ErrorAction SilentlyContinue) $events += @(Get-Event -SourceIdentifier $stopSourceId -ErrorAction SilentlyContinue) foreach ($ev in $events) { # Deduplicate — skip events we've already handled if (-not $script:seenEvents.Add($ev.EventIdentifier)) { continue } Remove-Event -EventIdentifier $ev.EventIdentifier -ErrorAction SilentlyContinue # Skip events that aren't ours (other subscriptions in the session) if ($ev.SourceIdentifier -ne $startSourceId -and $ev.SourceIdentifier -ne $stopSourceId) { continue } $props = $ev.SourceEventArgs.NewEvent $procPid = [uint32]$props.ProcessID $procName = [string]$props.ProcessName $parentId = [uint32]$props.ParentProcessId $isStart = ($ev.SourceIdentifier -eq $startSourceId) $timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff') if ($isStart) { # ── Process started ── $path = Get-ProcessPath -ProcessId $procPid if ((Test-Whitelisted -Path $path) -or (Test-Whitelisted -Path $procName)) { # Remember it's whitelisted so we also suppress exit $script:pidPathMap[$procPid] = '_WHITELISTED_' continue } # Cache path and start time for exit lookup $script:pidPathMap[$procPid] = $path $script:pidStartMap[$procPid] = [datetime]::Now # Resolve command line $cmdLine = Get-ProcessCommandLine -ProcessId $procPid $script:pidCmdMap[$procPid] = $cmdLine $displayPath = if ($path) { $path } else { "(path unavailable)" } $line = "[$timestamp] START PID=$procPid PPID=$parentId Name=$procName Path=$displayPath" Write-Host $line -ForegroundColor Green Write-Log $line # ── Command line ── $displayCmd = if ($cmdLine) { $cmdLine } else { "(unavailable)" } $cmdLineText = " |-- CmdLine: $displayCmd" Write-Host $cmdLineText -ForegroundColor DarkYellow Write-Log $cmdLineText # ── Parent process details ── $parentPath = Get-ProcessPath -ProcessId $parentId $parentName = $null try { $parentProc = Get-Process -Id $parentId -ErrorAction Stop $parentName = $parentProc.Name } catch { } if (-not $parentName) { # Check if we cached it from an earlier start event $cachedParent = $script:pidPathMap[$parentId] if ($cachedParent -and $cachedParent -ne '_WHITELISTED_') { $parentName = [System.IO.Path]::GetFileNameWithoutExtension($cachedParent) } } $parentDisplayPath = if ($parentPath) { $parentPath } else { "(path unavailable)" } $parentDisplayName = if ($parentName) { $parentName } else { "(unknown)" } $parentLine = " ^-- Parent: PID=$parentId Name=$parentDisplayName Path=$parentDisplayPath" Write-Host $parentLine -ForegroundColor DarkCyan Write-Log $parentLine }else { # ── Process exited ── $cachedPath = $script:pidPathMap[$procPid] # Suppress exit for whitelisted processes if ($cachedPath -eq '_WHITELISTED_') { $script:pidPathMap.Remove($procPid) $script:pidStartMap.Remove($procPid) $script:pidCmdMap.Remove($procPid) continue } # Whitelist-check by cached path and by process name if ((Test-Whitelisted -Path $cachedPath) -or (Test-Whitelisted -Path $procName)) { $script:pidPathMap.Remove($procPid) $script:pidStartMap.Remove($procPid) $script:pidCmdMap.Remove($procPid) continue } $displayPath = if ($cachedPath) { $cachedPath } else { "(path unavailable)" } $exitCode = try { [uint32]$props.ExitStatus } catch { '?' } # Calculate run duration if we captured the start time $durationStr = '' $startTime = $script:pidStartMap[$procPid] if ($startTime) { $duration = [datetime]::Now - $startTime $durationStr = " Ran for {0:d2}:{1:d2}:{2:d2}.{3:d3}" -f ` [int][math]::Floor($duration.TotalHours), $duration.Minutes, $duration.Seconds, $duration.Milliseconds } $line = "[$timestamp] EXIT PID=$procPid PPID=$parentId Name=$procName Path=$displayPath ExitCode=$exitCode$durationStr" Write-Host $line -ForegroundColor DarkGray Write-Log $line # ── Command line on exit ── $cachedCmd = $script:pidCmdMap[$procPid] $displayCmd = if ($cachedCmd) { $cachedCmd } else { "(unavailable)" } $cmdLineText = " |-- CmdLine: $displayCmd" Write-Host $cmdLineText -ForegroundColor DarkGray Write-Log $cmdLineText # Free tracking entries $script:pidPathMap.Remove($procPid) $script:pidStartMap.Remove($procPid) $script:pidCmdMap.Remove($procPid) } } # Periodic cleanup of the seen-events set to avoid unbounded growth if ($script:seenEvents.Count -gt 10000) { $script:seenEvents.Clear() } } } finally { # Clean up on exit Write-Host "`nStopping process monitor..." -ForegroundColor Yellow Unregister-Event -SourceIdentifier $startSourceId -ErrorAction SilentlyContinue Unregister-Event -SourceIdentifier $stopSourceId -ErrorAction SilentlyContinue Remove-Event -SourceIdentifier $startSourceId -ErrorAction SilentlyContinue Remove-Event -SourceIdentifier $stopSourceId -ErrorAction SilentlyContinue Write-Log "--- Monitor stopped at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ---" Write-Host "Cleaned up. Goodbye." -ForegroundColor Green }
Search Keywords: procmon, Process Monitor, Watcher




©2024 - Some portions of this website are Copyrighted.
Your IP: 216.73.216.4     Referring URL:
Browser: Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
Terms and Conditions, Privacy Policy, and Security Policy