#!/usr/bin/env python3 """Volatile evidence collection agent for compromised hosts. Collects volatile forensic artifacts following RFC 3227 order of volatility: registers, memory, network state, running processes, disk, and logs. Uses platform-native tools for live response evidence gathering. """ import sys import json import os import datetime import subprocess import shlex import platform import hashlib VOLATILITY_ORDER = [ {"priority": 1, "source": "memory", "description": "Physical memory (RAM) dump", "tool_linux": "avml", "tool_windows": "winpmem_mini_x64.exe"}, {"priority": 2, "source": "network_connections", "description": "Active network connections", "tool_linux": "ss -tunap", "tool_windows": "netstat -anob"}, {"priority": 3, "source": "running_processes", "description": "Running process list with details", "tool_linux": "ps auxwwf", "tool_windows": "tasklist /V /FO CSV"}, {"priority": 4, "source": "open_files", "description": "Open file handles", "tool_linux": "lsof -nP", "tool_windows": "handle64.exe -a"}, {"priority": 5, "source": "network_config", "description": "Network interface configuration", "tool_linux": "ip addr show", "tool_windows": "ipconfig /all"}, {"priority": 6, "source": "routing_table", "description": "Network routing table", "tool_linux": "ip route show", "tool_windows": "route print"}, {"priority": 7, "source": "arp_cache", "description": "ARP cache entries", "tool_linux": "ip neigh show", "tool_windows": "arp -a"}, {"priority": 8, "source": "dns_cache", "description": "DNS resolver cache", "tool_linux": "cat /etc/resolv.conf", "tool_windows": "ipconfig /displaydns"}, {"priority": 9, "source": "logged_users", "description": "Currently logged-in users", "tool_linux": "w", "tool_windows": "query user"}, {"priority": 10, "source": "scheduled_tasks", "description": "Scheduled tasks and cron jobs", "tool_linux": ["crontab -l", "ls /etc/cron.d/"], "tool_windows": "schtasks /query /FO CSV /V"}, ] def _run_single_cmd(cmd_str, timeout=60): """Run a single command string without shell, return stdout and stderr.""" return subprocess.run( shlex.split(cmd_str), capture_output=True, text=True, timeout=timeout ) def collect_artifact(source_config, output_dir): """Collect a single volatile artifact.""" is_windows = platform.system() == "Windows" cmd = source_config["tool_windows"] if is_windows else source_config["tool_linux"] source_name = source_config["source"] output_file = os.path.join(output_dir, source_name + ".txt") result = { "source": source_name, "priority": source_config["priority"], "command": cmd if isinstance(cmd, str) else "; ".join(cmd), "timestamp": datetime.datetime.utcnow().isoformat() + "Z", "status": "pending", } try: # Run multiple commands sequentially if cmd is a list, combining output if isinstance(cmd, list): combined_stdout = "" combined_stderr = "" last_rc = 0 for sub_cmd in cmd: sub_proc = _run_single_cmd(sub_cmd, timeout=60) combined_stdout += sub_proc.stdout combined_stderr += sub_proc.stderr last_rc = sub_proc.returncode proc = type("CombinedResult", (), { "stdout": combined_stdout, "stderr": combined_stderr, "returncode": last_rc, })() else: proc = _run_single_cmd(cmd, timeout=60) result["status"] = "collected" result["output_lines"] = len(proc.stdout.splitlines()) result["output_file"] = output_file result["exit_code"] = proc.returncode with open(output_file, "w", encoding="utf-8") as f: f.write("# Collected: {}\n".format(result["timestamp"])) f.write("# Command: {}\n".format(cmd)) f.write("# Exit code: {}\n\n".format(proc.returncode)) f.write(proc.stdout) if proc.stderr: f.write("\n# STDERR:\n" + proc.stderr) sha256 = hashlib.sha256(proc.stdout.encode()).hexdigest() result["sha256"] = sha256 except subprocess.TimeoutExpired: result["status"] = "timeout" except Exception as e: result["status"] = "error" result["error"] = str(e) return result def run_collection(output_dir, sources=None): """Run full volatile evidence collection.""" os.makedirs(output_dir, exist_ok=True) if sources is None: sources = VOLATILITY_ORDER manifest = { "collection_start": datetime.datetime.utcnow().isoformat() + "Z", "hostname": platform.node(), "platform": platform.system(), "output_dir": output_dir, "artifacts": [], } for source_config in sorted(sources, key=lambda x: x["priority"]): if source_config["source"] == "memory": manifest["artifacts"].append({ "source": "memory", "priority": 1, "status": "skipped", "note": "Memory dump requires elevated privileges and dedicated tool", }) continue result = collect_artifact(source_config, output_dir) manifest["artifacts"].append(result) manifest["collection_end"] = datetime.datetime.utcnow().isoformat() + "Z" manifest["total_collected"] = sum(1 for a in manifest["artifacts"] if a["status"] == "collected") manifest_path = os.path.join(output_dir, "collection_manifest.json") with open(manifest_path, "w", encoding="utf-8") as f: json.dump(manifest, f, indent=2) manifest["manifest_file"] = manifest_path return manifest if __name__ == "__main__": print("=" * 60) print("Volatile Evidence Collection Agent") print("RFC 3227 order of volatility, live response artifacts") print("=" * 60) print(" Platform: {}".format(platform.system())) print(" Hostname: {}".format(platform.node())) output_dir = sys.argv[1] if len(sys.argv) > 1 else os.path.join( os.path.expanduser("~"), "volatile_evidence_" + datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%S") ) print("\n--- RFC 3227 Volatility Order ---") for v in VOLATILITY_ORDER: tool = v["tool_windows"] if platform.system() == "Windows" else v["tool_linux"] print(" P{}: {:25s} [{}]".format(v["priority"], v["source"], tool)) print("\n[*] Collecting volatile evidence to: {}".format(output_dir)) manifest = run_collection(output_dir) print("\n--- Collection Results ---") for a in manifest["artifacts"]: status_marker = "+" if a["status"] == "collected" else "-" print(" [{}] P{}: {} -> {}".format( status_marker, a["priority"], a["source"], a["status"])) print("\nTotal collected: {}/{}".format( manifest["total_collected"], len(manifest["artifacts"]))) print("Manifest: {}".format(manifest.get("manifest_file", ""))) print("\n" + json.dumps({"artifacts_collected": manifest["total_collected"]}, indent=2))