Read an EDR Process Tree to Triage Malicious vs Benign Alerts
For developers and responders who need to close or escalate an EDR alert fast. This shows how to read the process tree, verify parent-child relationships on the host, and decide whether the alert is likely malicious, suspicious, or expected without guessing.
TL;DR — To decide whether an EDR alert is malicious, start with the process tree, not the detection name. The single most useful check is whether the parent-child chain matches a legitimate user action or scheduled task; if the tree shows an unusual launcher chain like
winword.exe -> powershell.exe -> cmd.exe -> curl.exeornginx -> sh -c -> curl, treat it as suspicious until you prove the command line, signer, path, and network destination are expected. Reading time: ~5 min
Goal
When you finish, you will be able to take an EDR alert, read the process tree from root to leaf, validate the same lineage on the endpoint with native OS commands, and classify the alert as benign, suspicious, or likely malicious with evidence you can paste into the case.
Prerequisites
- Access to your EDR alert details, including process tree, command line, hashes, username, hostname, and timestamps
- Shell access on the affected endpoint or equivalent forensic access
- Windows: PowerShell 5.1+ or PowerShell 7+ — check with:
$PSVersionTable.PSVersion
- Linux:
bash,ps,pstree,readlink,ss,sha256sum— check with:
bash --version | head -1
pstree --version
ps --version
- macOS: Terminal,
ps,lsof,codesign— check with:
sw_vers
codesign --version
- The alert timestamp in UTC and the host timezone offset
- The expected software on the host: deployment agent, package manager, CI runner, RMM tool, browser, Office apps, shell scripts, cron/systemd timers, login scripts
Steps
Step 1: Pull the minimum fields from the alert
Record these exact fields from the EDR alert into your notes or ticket:
{
"hostname": "HOST123",
"username": "alice",
"alert_time_utc": "2026-10-30T12:41:18Z",
"process_name": "powershell.exe",
"pid": 4820,
"parent_name": "winword.exe",
"parent_pid": 3116,
"command_line": "powershell.exe -nop -w hidden -enc SQBmACgA...",
"path": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"sha256": "<hash>",
"signer": "Microsoft Windows",
"destination": "198.51.100.24:443"
}
Success looks like a complete set of fields for the alerted process and at least its direct parent.
Step 2: Read the tree from root to leaf and label each hop
Build a simple chain from the EDR tree. Use this pattern and annotate each edge with why it happened.
View as text
[explorer.exe] ---- 1. user opened doc ----> [winword.exe] [winword.exe] ---- 2. spawned script host ----> [powershell.exe] [powershell.exe] ---- 3. launched shell ----> [cmd.exe] [cmd.exe] ---- 4. fetched remote content ----> [curl.exe]
Use these labels when reading the chain:
- expected launcher:
explorer.exe,services.exe,systemd,launchd, browser renderer, package manager - unusual launcher: Office app, PDF reader, browser plugin host,
rundll32,regsvr32,mshta,wmic,bash -c,sh -c - expected child: updater, helper process, renderer, signed app binary in its install path
- unusual child: shell, script engine, LOLBin, archive tool, downloader, temp-path executable
Success looks like a 3-6 process chain where each parent-child relationship is labeled expected or unusual.
Step 3: Validate the process lineage on the host
On Linux, inspect the current or recent lineage:
pstree -asp <pid>
ps -o pid,ppid,user,lstart,tty,stat,cmd -p <pid> -p <parent_pid>
readlink -f /proc/<pid>/exe
tr '\0' ' ' < /proc/<pid>/cmdline; echo
Example output shape:
$ pstree -asp 22184
systemd,1 --user
`-bash,21802
`-python3,22184 /opt/app/scripts/report.py --sync
$ ps -o pid,ppid,user,lstart,tty,stat,cmd -p 22184 -p 21802
PID PPID USER STARTED TT STAT CMD
21802 21790 deploy Fri Oct 30 12:40:58 2026 pts/0 Ss -bash
22184 21802 deploy Fri Oct 30 12:41:02 2026 pts/0 Sl python3 /opt/app/scripts/report.py --sync
On Windows:
Get-CimInstance Win32_Process -Filter "ProcessId=<pid> OR ProcessId=<parent_pid>" | Select-Object ProcessId,ParentProcessId,Name,CreationDate,ExecutablePath,CommandLine | Format-List
Get-Process -Id <pid>,<parent_pid> | Select-Object Id,ProcessName,Path,StartTime
Example output shape:
ProcessId : 4820
ParentProcessId : 3116
Name : powershell.exe
CreationDate : 20261030124118.123456+000
ExecutablePath : C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
CommandLine : powershell.exe -nop -w hidden -enc SQBmACgA...
On macOS:
ps -o pid,ppid,user,lstart,command -p <pid> -p <parent_pid>
lsof -p <pid> | head -20
codesign -dv --verbose=4 /proc/does-not-exist 2>/dev/null || true
codesign -dv --verbose=4 "$(ps -o command= -p <pid> | awk '{print $1}')" 2>&1 | sed -n '1,12p'
Success looks like the host data matching the EDR PID, PPID, start time, path, and command line within a few seconds of the alert timestamp.
Step 4: Check the four high-signal indicators
Run the exact checks for path, signer, command line, and network.
Windows signer and hash:
Get-AuthenticodeSignature "C:\path\to\binary.exe" | Format-List Status,StatusMessage,SignerCertificate
Get-FileHash "C:\path\to\binary.exe" -Algorithm SHA256
Linux hash and package ownership:
sha256sum /path/to/binary
command -v dpkg >/dev/null && dpkg -S /path/to/binary || true
command -v rpm >/dev/null && rpm -qf /path/to/binary || true
Network connections:
ss -tpn | grep <pid>
Get-NetTCPConnection -OwningProcess <pid> | Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort,State
What to look for:
- Path: expected install path is lower risk;
%TEMP%,C:\Users\<user>\AppData\Local\Temp,/tmp,/var/tmp, mounted shares are higher risk - Signer: valid signature helps, but signed LOLBins still abuse trust
- Command line:
-enc,-nop,-w hidden,IEX,FromBase64String,curl | sh,chmod +x /tmp/*,rundll32 javascript:are high signal - Network: first-time outbound to a raw IP, uncommon port, or newly registered domain is higher risk than your package mirror or artifact registry
Success looks like a short evidence list: exact path, signature status, hash, and remote endpoint.
Step 5: Classify the alert with a repeatable rule
Use this decision table.
| Condition | Classification | Action |
|---|---|---|
| Parent-child chain matches user action or scheduled task, binary path is expected, signer/hash are known, destination is approved | Benign | Close with evidence |
| One unusual hop exists but command line and destination map to internal automation or known admin tooling | Suspicious but expected | Close with tuning note |
| Unusual launcher plus shell/script engine plus encoded/hidden command or temp-path binary or unknown network | Likely malicious | Isolate host and escalate |
| Tree is incomplete or PID reused and you cannot validate path/command line | Inconclusive | Pull more telemetry before closing |
Use this case note template:
View as text
Classification: Likely malicious Reason: Parent-child chain winword.exe -> powershell.exe -> cmd.exe -> curl.exe does not match normal document handling. powershell.exe used -nop -w hidden -enc. curl.exe connected to 198.51.100.24:443. No approved automation on host HOST123 for user alice at 2026-10-30T12:41:18Z. Evidence: PID 4820, PPID 3116, path C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe, signer Microsoft Windows, SHA256 <hash>.
Success looks like a case note that another engineer can read and reach the same conclusion.
Step 6: If it is likely malicious, preserve evidence before cleanup
⚠️ Killing processes or deleting files can destroy evidence and break your timeline. If your incident process requires containment first, isolate the host in the EDR before terminating anything.
Windows quick capture:
Get-CimInstance Win32_Process -Filter "ProcessId=<pid>" | Select-Object * > C:\Temp\proc-<pid>.txt
Get-FileHash "C:\path\to\binary.exe" -Algorithm SHA256 > C:\Temp\hash-<pid>.txt
Get-NetTCPConnection -OwningProcess <pid> > C:\Temp\net-<pid>.txt
Linux quick capture:
ps -fp <pid> > /tmp/proc-<pid>.txt
sha256sum /path/to/binary > /tmp/hash-<pid>.txt
ss -tpn > /tmp/net-<pid>.txt
Success looks like three local evidence files saved before containment actions.
Verify it works
Use these checks to prove your conclusion is based on the tree and host facts, not the alert label.
For a benign case, you should be able to show a clean chain and expected destination:
pstree -asp <pid>
ss -tpn | grep <pid>
Expected shape:
systemd,1
`-cron,902
`-bash,1142 /opt/jobs/backup.sh
`-pg_dump,1188 -Fc appdb
And the remote endpoint should be your known backup target or there should be no network connection.
For a likely malicious case, you should be able to show at least two independent red flags:
Get-CimInstance Win32_Process -Filter "ProcessId=<pid> OR ProcessId=<parent_pid>" | Select ProcessId,ParentProcessId,Name,CommandLine
Get-NetTCPConnection -OwningProcess <pid>
Expected shape:
ProcessId ParentProcessId Name CommandLine
--------- --------------- ---- -----------
3116 2780 WINWORD.EXE "C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE" invoice.docm
4820 3116 powershell.exe powershell.exe -nop -w hidden -enc SQBmACgA...
LocalAddress LocalPort RemoteAddress RemotePort State
------------ --------- ------------- ---------- -----
10.0.2.15 49722 198.51.100.24 443 Established
If you cannot reproduce the lineage because the process exited, verify with the EDR timestamp, command line, and file hash from the alert, then mark the result inconclusive instead of closing as benign.
Common pitfalls
Treating the detection name as the verdict
Mistake: Closing or escalating based only on labels like "PowerShell suspicious" or "Living off the land". Symptom: False positives on admin scripts, or missed malware using signed binaries. Fix: Rebuild the parent-child chain and validate path, command line, signer, and destination before deciding.
Ignoring the root process
Mistake: Looking only at the leaf process that triggered the alert.
Symptom: You miss that powershell.exe came from explorer.exe running an approved script, or from winword.exe opening a macro-laced document.
Fix: Start at the root launcher and explain every hop to the leaf.
Trusting a valid signature too much
Mistake: Marking a process benign because the binary is signed by Microsoft, Apple, or a known vendor.
Symptom: Abuse of powershell.exe, rundll32.exe, mshta.exe, osascript, curl, or /bin/sh gets closed incorrectly.
Fix: Keep the signer as one signal only; weigh command-line flags, parent process, path, and network more heavily.
Missing PID reuse and exited processes
Mistake: Comparing a current process with the same PID long after the alert fired.
Symptom: Host-side ps output does not match the EDR command line or start time.
Fix: Compare CreationDate or lstart to the alert timestamp; if they differ, use EDR telemetry and classify as inconclusive if needed.
Not accounting for automation and software deployment tools
Mistake: Escalating every sh -c, powershell.exe, or curl child process.
Symptom: Noise from CI runners, config management, package post-install scripts, browser updaters, and RMM agents.
Fix: Check whether the parent is your known agent path and service account, and whether the destination is your artifact registry, package mirror, or management server.
Overlooking temp-path execution
Mistake: Focusing on the process name and ignoring where it ran from.
Symptom: chrome.exe, svchost.exe, or python appears harmless even though it launched from %TEMP% or /tmp.
Fix: Record the full executable path with ExecutablePath or /proc/<pid>/exe; temp-path execution is a strong escalation signal.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI