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
}
|