Day 17/90: Working with Processes -- Python Security

Day 17/90: Working with Processes -- Python Security

90 Day Python Security Scripting Challenge

Process analysis is core to incident response on any operating system. Today we use psutil to build practical triage tooling.

Process Inventory with Security Context

During triage you need more than just a process name. You need the owning user, the full command line, parent PID for process tree reconstruction, and the executable path to verify it is running from a legitimate location.

import psutil
from datetime import datetime

def security_inventory():
    """Build a process inventory with fields relevant to IR triage."""
    inventory = []
    for proc in psutil.process_iter(
        ['pid', 'ppid', 'name', 'exe', 'username', 'cmdline', 'create_time']
    ):
        try:
            info = proc.info
            inventory.append({
                'pid': info['pid'],
                'ppid': info['ppid'],
                'name': info['name'],
                'exe_path': info['exe'],
                'user': info['username'],
                'cmdline': ' '.join(info['cmdline'] or []),
                'age_seconds': (
                    datetime.now() - datetime.fromtimestamp(info['create_time'])
                ).total_seconds(),
            })
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue
    return inventory

recent = [p for p in security_inventory() if p['age_seconds'] < 300]
for p in recent:
    exe = p['exe_path'] or 'unknown'
    suspicious_paths = ['/tmp', '/dev/shm', 'AppData/Local/Temp']
    flag = ' [SUSPICIOUS PATH]' if any(sp in exe for sp in suspicious_paths) else ''
    print(f"NEW: PID={p['pid']} user={p['user']} exe={exe}{flag}")

Adding parent PID lets you reconstruct process trees. If PID 4500 spawned from PID 1200 which is svchost.exe, that tells a different story than if it spawned from cmd.exe running under a web server user.

Continuous Resource Monitoring

A single snapshot misses intermittent activity. Real monitoring samples repeatedly and builds a statistical baseline.

import psutil
import time
import statistics

def monitor_resources(duration=60, sample_interval=5):
    """Monitor process resources over a time window and report outliers."""
    cpu_history = {}
    for elapsed in range(0, duration, sample_interval):
        for proc in psutil.process_iter(['pid', 'name']):
            try:
                cpu = proc.cpu_percent(interval=0.05)
                pid = proc.info['pid']
                if pid not in cpu_history:
                    cpu_history[pid] = {'name': proc.info['name'], 'samples': []}
                cpu_history[pid]['samples'].append(cpu)
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                continue
        time.sleep(sample_interval)
    outliers = []
    for pid, data in cpu_history.items():
        if len(data['samples']) >= 3:
            avg = statistics.mean(data['samples'])
            if avg > 25:
                outliers.append({
                    'pid': pid, 'name': data['name'],
                    'avg_cpu': round(avg, 1),
                    'samples': len(data['samples'])
                })
    return sorted(outliers, key=lambda x: x['avg_cpu'], reverse=True)

for proc in monitor_resources(duration=30, sample_interval=5):
    print(f"OUTLIER: {proc['name']} PID={proc['pid']} avg_cpu={proc['avg_cpu']}%")

Network Connection Forensics

The most common question during an incident: what is this process talking to?

import psutil

def external_connections(
    private_prefixes=('10.', '172.16.', '172.17.', '172.18.', '172.19.',
                      '172.20.', '172.21.', '172.22.', '172.23.', '172.24.',
                      '172.25.', '172.26.', '172.27.', '172.28.', '172.29.',
                      '172.30.', '172.31.', '192.168.', '127.')
):
    """Find processes with connections to non-private IP addresses."""
    external = []
    for conn in psutil.net_connections(kind='inet'):
        if not conn.raddr or conn.pid is None:
            continue
        rip = conn.raddr.ip
        if any(rip.startswith(prefix) for prefix in private_prefixes):
            continue
        try:
            proc = psutil.Process(conn.pid)
            external.append({
                'pid': conn.pid,
                'process': proc.name(),
                'user': proc.username(),
                'remote': f"{rip}:{conn.raddr.port}",
                'status': conn.status,
            })
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            external.append({
                'pid': conn.pid, 'process': 'unknown',
                'remote': f"{rip}:{conn.raddr.port}",
                'status': conn.status
            })
    return external

for conn in external_connections():
    print(f"{conn['process']} (PID {conn['pid']}) -> {conn['remote']} [{conn['status']}]")

Safe Process Termination

The IR-safe kill pattern: freeze, document, terminate.

import psutil
import json
from pathlib import Path
from datetime import datetime, timezone

def ir_terminate(pid, case_id, analyst):
    """Freeze process, capture state, then terminate with chain of custody."""
    proc = psutil.Process(pid)
    proc.suspend()
    state = {
        'case_id': case_id,
        'analyst': analyst,
        'timestamp': datetime.now(timezone.utc).isoformat(),
        'process': {
            'pid': pid, 'name': proc.name(),
            'exe': proc.exe(), 'cmdline': proc.cmdline(),
            'user': proc.username()
        },
        'network': [
            {'remote': f"{c.raddr.ip}:{c.raddr.port}" if c.raddr else None}
            for c in proc.net_connections()
        ],
        'files': [f.path for f in proc.open_files()],
        'children': [c.pid for c in proc.children(recursive=True)],
    }
    evidence_dir = Path('evidence') / case_id
    evidence_dir.mkdir(parents=True, exist_ok=True)
    evidence_file = evidence_dir / f"proc_{pid}.json"
    evidence_file.write_text(json.dumps(state, indent=2))
    proc.terminate()
    return str(evidence_file)

Key Takeaways

Process triage with psutil gives you structured, cross-platform data that is far more reliable than parsing command output. The suspend-snapshot-terminate pattern preserves evidence. Multi-sample resource monitoring reduces false positives compared to single snapshots.

Day 17/90 of the 90 Day Python Security Scripting Challenge

Story originally reported by Dev.to. View at Dev.to →
← Back to all news