PowerShell Quick Guide: Process Investigation

PowerShell process investigation sits between two extremes: built-in Get-Process tells you almost nothing useful for triage, while a full EDR product does the job but is not always available on the box in front of you. The middle ground — a handful of Get-CimInstance and Get-NetTCPConnection queries — is what we reach for first when something looks off on a Windows endpoint and we need to decide whether to escalate.

Key Takeaways

  • Get-Process alone is not enough for defensive work; the command line, parent PID, and signature live on Win32_Process via Get-CimInstance.
  • Parent-child relationships are the fastest way to spot living-off-the-land abuse, like Office or Outlook spawning powershell.exe.
  • Command-line inspection catches encoded PowerShell, suspicious arguments, and binaries running out of %TEMP% or %APPDATA%.
  • Authenticode signature checks separate signed Microsoft binaries from third-party or unsigned files in unusual locations.
  • Use these techniques for authorised triage on systems you administer. For production monitoring, integrate the same signals into Microsoft Defender for Endpoint or another EDR rather than relying on ad-hoc scripts.

Environment

  • Windows 10 22H2 or Windows 11 23H2 endpoint, Windows Server 2019 or later.
  • Windows PowerShell 5.1 or PowerShell 7.4 — every cmdlet used below works on both.
  • Local administrator rights, since command lines and signature paths require elevation for non-current-user processes.
  • Optional but useful: Sysmon for richer process telemetry, Microsoft Defender for Endpoint for production-grade hunting.

The Problem

Most blog posts about "PowerShell threat hunting" stop at Get-Process | Sort-Object CPU -Descending, which is fine for finding the browser tab eating your battery but tells you nothing about an attacker. The information a defender actually wants — who started this, with what arguments, signed by whom, talking to which IP — is split across at least four different sources: the WMI/CIM Win32_Process class, Authenticode signature data, the network stack via Get-NetTCPConnection, and registry-based startup locations.

The goal of this guide is to wire those sources together into a few short scripts you can run during an authorised incident triage, in a lab, or on a system you own to baseline normal behaviour.

The Solution

Step 1 — Get the data that Get-Process hides

Get-Process returns a .NET System.Diagnostics.Process object that does not include the command line or parent PID. Get-CimInstance Win32_Process does, and it returns properties as plain values that play well with Select-Object:

Get-CimInstance Win32_Process |
    Select-Object ProcessId,
                  ParentProcessId,
                  Name,
                  CommandLine,
                  CreationDate,
                  @{Name='Owner';Expression={ ($_ | Invoke-CimMethod -MethodName GetOwner).User }} |
    Sort-Object CreationDate -Descending

Note that CommandLine is $null for processes you do not have rights to read — usually services running as another user when you are not elevated. If you see a lot of empty command-line columns, re-run the console as administrator.

Step 2 — Map the parent-child tree

Attackers very often live off the land. winword.exe spawning cmd.exe spawning powershell.exe is the textbook macro-execution chain, and it is invisible until you draw the tree. Two small joins do the work:

$procs = Get-CimInstance Win32_Process
$procs |
    Select-Object ProcessId,
                  Name,
                  CommandLine,
                  @{Name='Parent';Expression={
                      ($procs | Where-Object ProcessId -eq $_.ParentProcessId).Name
                  }} |
    Where-Object Parent -in 'winword.exe','excel.exe','outlook.exe','powerpnt.exe' |
    Format-Table -AutoSize

An Office product is not supposed to be the parent of cmd.exe, powershell.exe, wscript.exe, mshta.exe, or regsvr32.exe. If the table is not empty, that is the lead you investigate first.

Step 3 — Inspect command lines for the obvious red flags

PowerShell encoded commands, -WindowStyle Hidden, -ExecutionPolicy Bypass, and outbound Invoke-WebRequest calls show up in command-line strings even when the script body never touches disk:

$flags = @(
    '-enc',           # short for -EncodedCommand
    '-encodedcommand',
    'frombase64string',
    'iex',
    'downloadstring',
    'downloadfile',
    '-w hidden',
    '-windowstyle hidden',
    '-nop',
    '-noprofile',
    '-executionpolicy bypass'
)

Get-CimInstance Win32_Process |
    Where-Object CommandLine |
    ForEach-Object {
        $cl = $_.CommandLine.ToLower()
        foreach ($f in $flags) {
            if ($cl -like "*$f*") {
                [PSCustomObject]@{
                    Pid     = $_.ProcessId
                    Parent  = $_.ParentProcessId
                    Name    = $_.Name
                    Trigger = $f
                    Command = $_.CommandLine
                }
                break
            }
        }
    }

False positives exist — your own deployment tooling may legitimately call -NoProfile -ExecutionPolicy Bypass. Baseline a clean system, list the expected hits, and treat anything outside that list as worth a second look.

Step 4 — Verify Authenticode signatures

Unsigned binaries living in %TEMP%, %APPDATA%, or %PROGRAMDATA% are statistically far more interesting than signed Microsoft binaries in C:\Windows\System32:

Get-Process |
    Where-Object Path |
    Select-Object Name,
                  Id,
                  Path,
                  @{Name='Signer';Expression={
                      (Get-AuthenticodeSignature $_.Path).SignerCertificate.Subject
                  }},
                  @{Name='Status';Expression={
                      (Get-AuthenticodeSignature $_.Path).Status
                  }} |
    Where-Object {
        $_.Path -like "$env:TEMP*" -or
        $_.Path -like "$env:APPDATA*" -or
        $_.Status -ne 'Valid'
    }

Combine the signature check with the parent-child tree from Step 2 and you have a short list of the processes worth investigating first.

Step 5 — Tie processes to network connections

A process running with no network activity is rarely interesting. A process you have never heard of with an established connection to a hosting-provider IP is worth a closer look:

Get-NetTCPConnection -State Established |
    Where-Object { $_.RemoteAddress -notmatch '^10\.|^172\.(1[6-9]|2[0-9]|3[01])\.|^192\.168\.|^127\.|^::1' } |
    Select-Object LocalPort,
                  RemoteAddress,
                  RemotePort,
                  @{Name='Process';Expression={
                      (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Name
                  }},
                  @{Name='Path';Expression={
                      (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Path
                  }} |
    Sort-Object RemoteAddress

The regex filters RFC 1918 ranges plus IPv4 loopback and IPv6 loopback, leaving only externally routable destinations. Be aware that legitimate update services and telemetry endpoints will show up here — baseline before you alert.

Step 6 — Check the persistence surface

If a suspicious process keeps coming back after a reboot, it has a persistence anchor somewhere. Win32_StartupCommand only covers the legacy startup folders and the basic Run keys, but it is a one-line first pass:

Get-CimInstance Win32_StartupCommand |
    Select-Object Name, Command, Location, User

Scheduled tasks and WMI event subscriptions are the next places to look — covered in our scheduled-task detection post linked below.

Frequently Asked Questions

Why does CommandLine come back empty for some processes?

Reading another user's process command line requires either local administrator rights or the same token as the process owner. Empty values almost always mean an unelevated console. Re-run as administrator.

Is Get-WmiObject still safe to use?

It works on 5.1 and produces the same data, but it is deprecated and absent from PowerShell 7. Use Get-CimInstance for new code — it talks WinRM rather than DCOM, plays better with firewalls, and is the supported path going forward.

Is this a replacement for an EDR?

No. These queries are point-in-time snapshots; an EDR captures the full process tree over time, correlates with file and registry activity, and stores the result for retrospective hunting. Use these scripts for triage on machines you administer or to learn what the signals look like. For continuous monitoring, deploy Microsoft Defender for Endpoint, Sysmon plus a SIEM, or an equivalent commercial agent.

How do I run this remotely against a fleet?

Wrap any of the snippets above in Invoke-Command -ComputerName $list -ScriptBlock { ... }. PowerShell remoting carries the objects back deserialised, so most of the downstream pipeline still works. See the remote management guide linked below.

Are these queries safe to run on a live production server?

Yes — they are read-only and add negligible CPU. The risk is operational: if you are connected over a slow link, Get-CimInstance Win32_Process over the wire can take longer than expected when the host has thousands of processes. Limit the query with -Filter for tight scopes.

Conclusion

Most useful endpoint triage comes from four signals: who started the process, what arguments it has, who signed the binary, and what it is talking to on the network. PowerShell exposes all four in a few cmdlets, and the patterns above are the ones we keep recycling. They are not a substitute for proper endpoint telemetry, but on a system you administer they are enough to decide whether something deserves a deeper look or a clean bill of health.

Save the snippets as a profile module, baseline a known-clean machine, and the next time something feels off you will get to a useful answer in minutes rather than hours.

Related Posts

Authoritative reference for the WMI class used above: Win32_Process on Microsoft Learn.

Editorial note: posts on this blog are drafted with AI assistance and then reviewed, edited, and tested against a real environment before publishing. Commands, output, and screenshots come from systems I actually ran the work on.

PowerShell Threat Hunting Tutorials
SecurityScriptographer author

About the author

SecurityScriptographer is written and maintained by one person — a defender who builds and tests the detections, scripts, and Microsoft 365 workflows here before publishing them. More about me · @twi_nox

0 comments:

Post a Comment