PowerShell for Responders
A quick reference for reading an endpoint from a remote response shell: get the data, filter it down, and format it so nothing truncates, without writing anything to disk.
On this page
- The shape of every command
- Verbs you will actually use
- Filtering: do it early
- Selecting fields
- Formatting so nothing truncates
- Why output shows the ellipsis
- Common flags
- Keeping output small
- Time handling
- String formatting for clean lines
- Aliases you will see
- Safety habits
- Triage starters
- Processes with command line and parent
- Network connections with the owning process
- Run keys, machine and per user
- Scheduled tasks that run something outside Windows
- Recent executables and scripts in user-writable paths
- Logons in the last 24 hours
The goal is to get a readable answer on screen without writing anything to the endpoint. That comes down to three skills: get the data, filter it down, and format it so nothing truncates.
The shape of every command
Get-Something | Where-Object { filter } | Select-Object fields | Sort-Object field | Format-List
Each | hands the objects from the left into the command on the right. PowerShell passes objects, not text, which is why you can filter on a property name instead of grepping.
Think of it as a conveyor belt:
Get-Something -> the belt starts loaded with objects
| Where-Object -> station 1: throw out the ones that don't qualify
| Select-Object -> station 2: keep only the parts you need
| Sort-Object -> station 3: put them in order
| Format-List -> station 4: package them to be read
Get-ChildItem "C:\Users\<user>\AppData\Local\Temp" -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -eq '.exe' } |
Select-Object Name, Length, CreationTimeUtc, FullName |
Sort-Object CreationTimeUtc -Descending |
Format-List
Verbs you will actually use
| Cmdlet | What it gets |
|---|---|
Get-CimInstance |
WMI data: processes, services, drivers, disks |
Get-ChildItem |
Files and folders (ls / dir are aliases) |
Get-Content |
The contents of a file (cat is an alias) |
Get-ItemProperty |
Registry values under a key |
Get-Item |
A single file, folder, or registry key object |
Select-String |
Pattern search inside files (grep equivalent) |
Get-FileHash |
SHA256 and other hashes |
Get-AuthenticodeSignature |
Code-signing status |
Get-ScheduledTask |
Scheduled tasks |
Get-NetTCPConnection |
Active TCP connections |
Get-WinEvent |
Event log records |
Get-Process |
Running processes. Lighter than CIM, but no command line in Windows PowerShell 5.1 (PowerShell 7 adds CommandLine) |
Filtering: do it early
Where-Object { $_.Name -match 'powershell|cmd' }
Where-Object Name -match 'powershell|cmd' # simplified syntax, same result
$_ means “the current object coming down the pipe.” The two forms are equivalent for a single comparison; use the braces version when you need more than one condition.
| Operator | Meaning |
|---|---|
-eq -ne |
equals, not equals |
-like |
wildcard match, use * |
-match |
regular expression match |
-notmatch |
regex non-match |
-in -contains |
membership in a list: $x -in $list, or $list -contains $x |
-gt -lt |
greater than, less than (works on dates) |
-and -or |
combine conditions |
Comparisons are case-insensitive by default. Prefix with c for case-sensitive (-cmatch).
Selecting fields
Select-Object ProcessId, Name, CommandLine # keep these properties
Select-Object -First 20 # first 20 objects
Select-Object -Last 20 # last 20
Select-Object -ExpandProperty Hash # return the bare value, not an object
A calculated property builds a new column. Here it adds the owning process name to each network connection:
Get-NetTCPConnection -State Established |
Select-Object LocalPort, RemoteAddress, RemotePort,
@{n='Process'; e={ (Get-Process -Id $_.OwningProcess).ProcessName }}
n is the column name, e is the expression that produces its value.
Formatting so nothing truncates
This is the single most important section for shell-based review.
| Command | Use it when |
|---|---|
Format-List |
Long values: command lines, paths, hashes. One property per line, never clipped. Default choice. |
Format-Table -AutoSize |
Short values, many rows: you want to scan a list |
Format-Table -AutoSize -Wrap |
Table, but let long cells wrap instead of clip |
Out-String -Width 4096 |
Force a wide render when the host reports a narrow console |
ConvertTo-Json -Depth 3 |
Structured output that ignores console width entirely |
ConvertTo-Csv -NoTypeInformation |
Easy to paste into case notes |
Why output shows the ellipsis
For objects without a built-in view, PowerShell picks Format-Table when there are four or fewer properties, and a table clips cells to the console width. The data is not lost; the renderer threw it away. Switch to Format-List and it comes back.
Collections inside a property are also cut off, after four items by default. Set this once at the start of a session so they print in full:
$FormatEnumerationLimit = -1
Common flags
| Flag | Meaning |
|---|---|
-Force |
Include hidden and system items |
-Recurse |
Descend into subfolders |
-LiteralPath |
Treat the path as literal. Required when it contains [ ] or other wildcard characters |
-ErrorAction SilentlyContinue (-EA 0) |
Suppress access-denied noise so the command completes |
-Filter |
Provider-level filter, much faster than piping into Where-Object |
-MaxEvents n |
Cap event log results |
-Depth n |
How far to recurse, or how deep to serialize |
-Raw |
Return a file as one string instead of an array of lines |
Use -ErrorAction SilentlyContinue on anything recursive across user profiles, or the first denied folder buries your output.
Keeping output small
Volume is the enemy of shell review. In order of preference:
- Filter at the source:
-Filter, or a specific path instead ofC:\ - Filter early in the pipe:
Where-ObjectbeforeSelect-Object - Cap the rows:
Select-Object -First 25 - Bound by time:
Where-Object LastWriteTimeUtc -gt (Get-Date).ToUniversalTime().AddHours(-24) - Count first, then look:
Measure-Objecttells you whether it is worth printing
Get-ChildItem "C:\Path" -Recurse -ErrorAction SilentlyContinue | Measure-Object | Select-Object Count
Page through a long result set by moving -Skip:
Get-ChildItem "C:\Path" -Recurse -ErrorAction SilentlyContinue |
Select-Object -Skip 0 -First 25 |
Format-Table Name, Length -AutoSize
Time handling
Always work in UTC. The Utc properties exist on file objects for exactly this reason.
(Get-Date).ToUniversalTime()
(Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ssZ')
Get-ChildItem "C:\Path" | Select-Object LastWriteTimeUtc, CreationTimeUtc, Name
.AddHours(-24) and .AddDays(-7) build relative windows.
String formatting for clean lines
When a table will not fit, build the line yourself:
Get-ChildItem "C:\Path" -File |
ForEach-Object { "{0} {1}" -f (Get-FileHash $_.FullName).Hash, $_.FullName }
-f fills {0} {1} in order. `t is a tab. The backtick is PowerShell’s escape character, and also continues a command onto the next line.
Aliases you will see
| Alias | Real cmdlet |
|---|---|
ls dir gci |
Get-ChildItem |
cat gc |
Get-Content |
ps gps |
Get-Process |
where ? |
Where-Object |
select |
Select-Object |
sort |
Sort-Object |
fl ft |
Format-List Format-Table |
Write cmdlets in full in case notes. Aliases are fine while typing, unreadable six months later.
Safety habits
- Never pipe an enumeration into
Remove-Item“to save a step” - Never use wildcards in a delete
-WhatIfon any destructive cmdlet shows what would happen without doing it- Read a script with
Get-Content; never run it to find out what it does
Triage starters
Ready-to-run, read-only one-liners that put the patterns above to work. Each one filters early, selects only what matters, and formats so nothing truncates.
Processes with command line and parent
$procs = Get-CimInstance Win32_Process
$names = @{}; $procs | ForEach-Object { $names[$_.ProcessId] = $_.Name }
$procs |
Select-Object ProcessId, Name,
@{n='Parent'; e={ $names[$_.ParentProcessId] }}, ParentProcessId, CommandLine |
Format-List
A parent that no longer exists shows up blank, which is worth noticing on its own.
Network connections with the owning process
Get-NetTCPConnection -State Established |
Select-Object RemoteAddress, RemotePort, LocalPort, OwningProcess,
@{n='Process'; e={ (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName }} |
Sort-Object RemoteAddress |
Format-Table -AutoSize
Run keys, machine and per user
In a remote response shell you usually run as SYSTEM, so HKCU is SYSTEM’s hive, not the user’s. Read per-user keys through HKEY_USERS instead:
$keys = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run',
'HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Run'
)
$keys += Get-ChildItem 'Registry::HKEY_USERS' |
Where-Object { $_.PSChildName -match '^S-1-5-21-[\d-]+$' } |
ForEach-Object { "Registry::HKEY_USERS\$($_.PSChildName)\Software\Microsoft\Windows\CurrentVersion\Run" }
$keys | ForEach-Object {
$key = $_
$item = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
if ($item) {
$item.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } |
ForEach-Object { [pscustomobject]@{ Key = $key; Name = $_.Name; Value = $_.Value } }
}
} | Format-List
Only hives of users with a loaded profile appear under HKEY_USERS.
Scheduled tasks that run something outside Windows
Filtering out the \Microsoft\ task folder misses attackers who hide tasks there. Filter on what the task executes instead:
Get-ScheduledTask | ForEach-Object {
$task = $_
$task.Actions | Where-Object { $_.Execute } | ForEach-Object {
[pscustomobject]@{
Task = "$($task.TaskPath)$($task.TaskName)"
State = $task.State
Execute = $_.Execute
Arguments = $_.Arguments
}
}
} | Where-Object { $_.Execute -notmatch '^"?(%windir%|%SystemRoot%|C:\\Windows\\)' } |
Format-List
Recent executables and scripts in user-writable paths
Count first (see above); this can be large on a busy profile.
$since = (Get-Date).ToUniversalTime().AddDays(-7)
Get-ChildItem 'C:\Users\*\AppData', 'C:\Users\Public', 'C:\ProgramData' -Recurse -Force -File `
-Include *.exe, *.dll, *.ps1, *.bat, *.cmd, *.vbs, *.js -ErrorAction SilentlyContinue |
Where-Object { $_.CreationTimeUtc -gt $since } |
Select-Object CreationTimeUtc, Length, FullName |
Sort-Object CreationTimeUtc -Descending |
Format-Table -AutoSize -Wrap
Logons in the last 24 hours
Interactive (2), network (3), and remote interactive (10) logons from the Security log:
Get-WinEvent -FilterHashtable @{ LogName = 'Security'; Id = 4624; StartTime = (Get-Date).AddHours(-24) } `
-MaxEvents 500 -ErrorAction SilentlyContinue |
ForEach-Object {
$data = @{}
([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $data[$_.Name] = $_.'#text' }
[pscustomobject]@{
TimeUtc = $_.TimeCreated.ToUniversalTime()
User = "$($data.TargetDomainName)\$($data.TargetUserName)"
LogonType = $data.LogonType
SourceIp = $data.IpAddress
Process = $data.ProcessName
}
} |
Where-Object { $_.LogonType -in '2', '3', '10' } |
Format-Table -AutoSize -Wrap
Remote interactive logons (type 10) with an unexpected source IP are usually the first thing to chase.