Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

176 lines
7.0 KiB
Python

#!/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))