Learning Goals
3 minBy the end of this lesson you can:
- Enumerate processes with
psutiland read their details. - List active network connections and map them to processes.
- Watch CPU/memory/disk to spot resource abuse (e.g. crypto-mining).
- Build a host snapshot you can baseline and diff for anomalies.
Warm-Up · The Security View of a Running System
5 minA compromised machine usually behaves differently: a process you don't recognise eating 100% CPU (a miner), an outbound connection to a strange IP (a beacon to a command server), a binary running from /tmp. The OS exposes all of this — you just have to look.
psutil is a cross-platform window into the live system: every process, its command line, the user running it, its open connections, and resource use. From a security angle it's a host-based detection tool — by knowing what normal looks like (a baseline), you can spot the abnormal: unexpected processes, suspicious connections, and resource spikes that signal abuse.
New Concept · Processes, Connections, Resources
14 minEnumerate processes
import psutil # pip install psutil for proc in psutil.process_iter(["pid", "name", "username", "cpu_percent"]): info = proc.info print(f"{info['pid']:>6} {info['username'] or '?':12} {info['name']}") # inspect one process in detail p = psutil.Process() # current process (or pass a pid) print(p.name(), p.exe(), p.cmdline()) # what it is + how it was launched print("opened files:", p.open_files()[:3]) print("parent:", p.ppid()) # process lineage matters for forensics
For security, the revealing fields are exe() (where the binary lives — /tmp is suspicious), cmdline() (the full launch command — miners often have tell-tale args), username() (running as root?), and ppid() (who started it).
Network connections — who is this machine talking to?
import psutil for conn in psutil.net_connections(kind="inet"): if conn.status == "ESTABLISHED" and conn.raddr: local = f"{conn.laddr.ip}:{conn.laddr.port}" remote = f"{conn.raddr.ip}:{conn.raddr.port}" # map the connection back to the process that owns it owner = psutil.Process(conn.pid).name() if conn.pid else "?" print(f"{owner:15} {local} → {remote}")
This is gold for detection: an unexpected outbound connection (especially from an odd process to a strange IP/port) can be a malware beacon phoning home to a command-and-control server. Mapping conn.pid to the process tells you what is talking.
Resource use — spot abuse
import psutil print("CPU %:", psutil.cpu_percent(interval=1)) print("memory %:", psutil.virtual_memory().percent) print("disk %:", psutil.disk_usage("/").percent) # top CPU consumers — a 100%-CPU mystery process is a classic miner sign procs = [(p.info["cpu_percent"], p.info["name"], p.info["pid"]) for p in psutil.process_iter(["name", "cpu_percent"])] for cpu, name, pid in sorted(procs, reverse=True)[:5]: print(f"{cpu:5.1f}% {name} (pid {pid})")
Sustained high CPU from an unknown process is the signature of crypto-mining malware; unexpected disk filling can be data staging before exfiltration. Resource monitoring catches abuse that doesn't open ports.
Monitoring detects; it doesn't prove malice. A high-CPU process might be a legitimate build. Investigate (check the path, signature, parent, network) before you kill or quarantine anything — and only ever on systems you administer. False positives erode trust just like missed detections.
A few more useful calls
psutil.users() # who is logged in (suspicious logins?) psutil.boot_time() # when the system started psutil.net_io_counters() # bytes sent/received (exfil = high outbound) p.connections() # connections owned by one process
Worked Example · A Host Snapshot for Baselining
12 minGoal: capture a structured snapshot of the host — processes, listening ports, established connections, resource use — that you can save as a baseline and later diff to spot what's new (the seed of Lesson 23's anomaly detection).
import psutil, json from datetime import datetime def snapshot() -> dict: procs = [] for p in psutil.process_iter(["pid", "name", "username", "exe"]): try: procs.append({"name": p.info["name"], "user": p.info["username"], "exe": p.info["exe"]}) except (psutil.NoSuchProcess, psutil.AccessDenied): continue # processes come and go / need privileges listening = sorted({c.laddr.port for c in psutil.net_connections("inet") if c.status == "LISTEN"}) established = [] for c in psutil.net_connections("inet"): if c.status == "ESTABLISHED" and c.raddr: established.append({"remote": f"{c.raddr.ip}:{c.raddr.port}", "pid": c.pid}) return { "captured": datetime.now().isoformat(), "process_names": sorted({p["name"] for p in procs}), "listening_ports": listening, "established_remotes": sorted({e["remote"] for e in established}), "cpu_percent": psutil.cpu_percent(interval=1), "mem_percent": psutil.virtual_memory().percent, } snap = snapshot() print(f"{len(snap['process_names'])} distinct processes") print("listening ports:", snap["listening_ports"]) print("outbound to:", snap["established_remotes"][:5]) print(f"CPU {snap['cpu_percent']}% MEM {snap['mem_percent']}%") # save as a baseline of 'normal': with open("host_baseline.json", "w") as f: json.dump(snap, f, indent=2)
142 distinct processes listening ports: [22, 631, 3000, 5432] outbound to: ['140.82.121.4:443', '142.250.66.78:443'] CPU 8.3% MEM 47.0% process names + ports + remotes saved → host_baseline.json
Read the code
This snapshot is a security-relevant fingerprint of the running host: what processes exist, what's listening, who it's talking to, and how loaded it is. Note the try/except around process access — processes vanish mid-iteration and some need privileges, so robust monitoring tolerates that. Captured at a known-good moment, this becomes a baseline of normal; in the next lesson we diff a fresh snapshot against it to flag anything new — a process that appeared, a port that opened, a strange remote address. That diff is host-based anomaly detection.
Try It Yourself
13 minList the top 5 processes by CPU and by memory on your own machine. Identify what each is — and whether you'd recognise an impostor among them.
List all ESTABLISHED connections with the owning process name and the remote address. Note which are expected (browser → :443) and reason about what an unexpected one might mean.
Hint
for c in psutil.net_connections("inet"): if c.status == "ESTABLISHED" and c.raddr and c.pid: try: name = psutil.Process(c.pid).name() except psutil.Error: name = "?" print(f"{name:15} → {c.raddr.ip}:{c.raddr.port}")
Write suspicious(proc) that flags a process if any heuristic is true: running from /tmp or a temp dir, no signature/odd name, very high CPU, or a long random-looking name. Run it over all processes and list anything flagged (expect false positives — that's the point to discuss).
Hint
import psutil def suspicious(p): reasons = [] try: exe = (p.exe() or "").lower() if "/tmp" in exe or "\\temp\\" in exe: reasons.append("runs from a temp dir") if p.cpu_percent() > 80: reasons.append("very high CPU") if len(p.name()) > 20 and p.name().isalnum(): reasons.append("long random-looking name") except psutil.Error: return [] return reasons
Mini-Challenge · A Live Resource-Abuse Watcher
8 minBuild a watcher that samples CPU and the top process every few seconds and prints an alert if any single process sustains >X% CPU for N consecutive samples — the signature of a crypto-miner. Avoid one-off false positives by requiring it to persist across samples.
Show a sample solution
import psutil, time from collections import defaultdict def watch(threshold=80, sustained=3, interval=2): streak = defaultdict(int) while True: for p in psutil.process_iter(["name", "cpu_percent"]): cpu = p.info["cpu_percent"] name = p.info["name"] if cpu > threshold: streak[name] += 1 if streak[name] >= sustained: print(f"⚠️ {name} sustained {cpu:.0f}% CPU " f"for {streak[name]} samples — investigate " f"(miner? runaway process?)") else: streak[name] = 0 # reset when it drops time.sleep(interval) # watch() # Ctrl-C to stop
Non-negotiables: per-process CPU tracking, a SUSTAINED requirement (not one spike), and an investigative (not auto-kill) alert.
Recap
3 minpsutil is your window into a live host: process_iter enumerates processes (with the security-revealing exe, cmdline, username, ppid), net_connections maps active connections to their owning process (spotting beacons to command servers), and CPU/memory/disk calls reveal resource abuse like crypto-mining. Capture a structured snapshot at a known-good moment to baseline "normal," then diff later snapshots for anomalies (next lesson). Monitoring detects — always investigate before acting, tolerate false positives, and only on systems you administer.
Vocabulary Card
- psutil
- A cross-platform library exposing processes, connections, and resources.
- beacon
- Malware's periodic outbound connection to a command-and-control server.
- host-based detection
- Spotting compromise from a single machine's own behaviour.
- baseline (host)
- A snapshot of normal processes/ports/connections to diff against.
Homework
4 minBuild a host-snapshot tool and capture a baseline of your own machine. Then build the resource-abuse watcher and run it while doing something CPU-heavy (a build, a video) to see it (correctly) flag a sustained spike — and reason about whether that's a true or false positive. Write a note: three host signals that could indicate compromise, and why you'd investigate rather than auto-act.
Sample · host compromise signals
Three signals worth investigating: 1. An unknown process at sustained high CPU — classic crypto-miner. (But: could be a legit build/render — check the binary path, signature, and parent before acting.) 2. An outbound ESTABLISHED connection from an odd process to a strange IP/port — possible C2 beacon. Check what the process is and whether the destination is expected. 3. A binary running from /tmp or a temp dir, or a long random-named process — malware often drops there. Verify the file's origin. Why investigate, not auto-act: monitoring DETECTS behaviour, it doesn't PROVE malice. My CPU watcher flagged 'blender' at 95% during a render — a true high-CPU reading but a FALSE alarm for "miner." Auto-killing it would've destroyed work. Confirm with context (path, signature, network, who launched it) first.
Non-negotiables: working snapshot baseline + sustained-spike watcher, a real (even false-positive) detection, and three compromise signals with the investigate-first reasoning.