#!/usr/bin/env python3 """ EZ Tools Forensic Artifact Processor Automates the execution of Eric Zimmerman's tools against collected forensic artifacts and generates consolidated analysis reports. """ import subprocess import csv import os import sys import json import hashlib from pathlib import Path from datetime import datetime from collections import defaultdict class EZToolsProcessor: """Orchestrates EZ Tools processing against forensic artifact collections.""" def __init__(self, ez_tools_path: str, evidence_path: str, output_path: str): self.ez_tools_path = Path(ez_tools_path) self.evidence_path = Path(evidence_path) self.output_path = Path(output_path) self.output_path.mkdir(parents=True, exist_ok=True) self.results = {} self.tools = { "MFTECmd": self.ez_tools_path / "MFTECmd.exe", "PECmd": self.ez_tools_path / "PECmd.exe", "LECmd": self.ez_tools_path / "LECmd.exe", "JLECmd": self.ez_tools_path / "JLECmd.exe", "SBECmd": self.ez_tools_path / "SBECmd.exe", "EvtxECmd": self.ez_tools_path / "EvtxECmd.exe", "RECmd": self.ez_tools_path / "RECmd.exe", "RBCmd": self.ez_tools_path / "RBCmd.exe", "AmcacheParser": self.ez_tools_path / "AmcacheParser.exe", "AppCompatCacheParser": self.ez_tools_path / "AppCompatCacheParser.exe", } def verify_tools(self) -> dict: """Verify which EZ Tools are available on the system.""" availability = {} for name, path in self.tools.items(): availability[name] = path.exists() return availability def run_tool(self, tool_name: str, args: list) -> dict: """Execute an EZ Tool with given arguments.""" tool_path = self.tools.get(tool_name) if not tool_path or not tool_path.exists(): return {"status": "error", "message": f"{tool_name} not found at {tool_path}"} cmd = [str(tool_path)] + args try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=300 ) return { "status": "success" if result.returncode == 0 else "error", "stdout": result.stdout, "stderr": result.stderr, "returncode": result.returncode } except subprocess.TimeoutExpired: return {"status": "error", "message": f"{tool_name} timed out after 300 seconds"} except Exception as e: return {"status": "error", "message": str(e)} def process_mft(self, mft_path: str = None) -> dict: """Parse the $MFT file with MFTECmd.""" if mft_path is None: mft_candidates = list(self.evidence_path.rglob("$MFT")) if not mft_candidates: return {"status": "skipped", "message": "$MFT not found"} mft_path = str(mft_candidates[0]) output_file = "MFT_output.csv" args = ["-f", mft_path, "--csv", str(self.output_path), "--csvf", output_file] result = self.run_tool("MFTECmd", args) result["output_file"] = str(self.output_path / output_file) self.results["MFT"] = result return result def process_usn_journal(self, journal_path: str = None) -> dict: """Parse the USN Journal ($J) with MFTECmd.""" if journal_path is None: j_candidates = list(self.evidence_path.rglob("$J")) if not j_candidates: return {"status": "skipped", "message": "$J not found"} journal_path = str(j_candidates[0]) output_file = "USNJournal_output.csv" args = ["-f", journal_path, "--csv", str(self.output_path), "--csvf", output_file] result = self.run_tool("MFTECmd", args) result["output_file"] = str(self.output_path / output_file) self.results["USNJournal"] = result return result def process_prefetch(self, prefetch_dir: str = None) -> dict: """Parse Prefetch files with PECmd.""" if prefetch_dir is None: pf_candidates = list(self.evidence_path.rglob("Prefetch")) if not pf_candidates: return {"status": "skipped", "message": "Prefetch directory not found"} prefetch_dir = str(pf_candidates[0]) output_file = "Prefetch_output.csv" args = ["-d", prefetch_dir, "--csv", str(self.output_path), "--csvf", output_file] result = self.run_tool("PECmd", args) result["output_file"] = str(self.output_path / output_file) self.results["Prefetch"] = result return result def process_lnk_files(self, lnk_dir: str = None) -> dict: """Parse LNK shortcut files with LECmd.""" if lnk_dir is None: recent_candidates = list(self.evidence_path.rglob("Recent")) if not recent_candidates: return {"status": "skipped", "message": "Recent directory not found"} lnk_dir = str(recent_candidates[0]) output_file = "LNK_output.csv" args = ["-d", lnk_dir, "--csv", str(self.output_path), "--csvf", output_file] result = self.run_tool("LECmd", args) result["output_file"] = str(self.output_path / output_file) self.results["LNK"] = result return result def process_jump_lists(self, jl_dir: str = None) -> dict: """Parse Jump List files with JLECmd.""" if jl_dir is None: auto_dest = list(self.evidence_path.rglob("AutomaticDestinations")) if not auto_dest: return {"status": "skipped", "message": "AutomaticDestinations not found"} jl_dir = str(auto_dest[0]) output_file = "JumpLists_output.csv" args = ["-d", jl_dir, "--csv", str(self.output_path), "--csvf", output_file] result = self.run_tool("JLECmd", args) result["output_file"] = str(self.output_path / output_file) self.results["JumpLists"] = result return result def process_event_logs(self, evtx_dir: str = None) -> dict: """Parse Windows Event Logs with EvtxECmd.""" if evtx_dir is None: logs_candidates = list(self.evidence_path.rglob("winevt")) if logs_candidates: evtx_dir = str(logs_candidates[0] / "Logs") else: evtx_files = list(self.evidence_path.rglob("*.evtx")) if not evtx_files: return {"status": "skipped", "message": "Event logs not found"} evtx_dir = str(evtx_files[0].parent) output_file = "EventLogs_output.csv" args = ["-d", evtx_dir, "--csv", str(self.output_path), "--csvf", output_file] result = self.run_tool("EvtxECmd", args) result["output_file"] = str(self.output_path / output_file) self.results["EventLogs"] = result return result def process_shellbags(self, registry_dir: str = None) -> dict: """Parse Shellbag artifacts with SBECmd.""" if registry_dir is None: registry_dir = str(self.evidence_path) output_file = "Shellbags_output.csv" args = ["-d", registry_dir, "--csv", str(self.output_path), "--csvf", output_file] result = self.run_tool("SBECmd", args) result["output_file"] = str(self.output_path / output_file) self.results["Shellbags"] = result return result def process_recycle_bin(self, recycle_dir: str = None) -> dict: """Parse Recycle Bin artifacts with RBCmd.""" if recycle_dir is None: rb_candidates = list(self.evidence_path.rglob("$Recycle.Bin")) if not rb_candidates: return {"status": "skipped", "message": "$Recycle.Bin not found"} recycle_dir = str(rb_candidates[0]) output_file = "RecycleBin_output.csv" args = ["-d", recycle_dir, "--csv", str(self.output_path), "--csvf", output_file] result = self.run_tool("RBCmd", args) result["output_file"] = str(self.output_path / output_file) self.results["RecycleBin"] = result return result def detect_timestomping(self, mft_csv_path: str) -> list: """Analyze MFT CSV output to detect timestomping indicators.""" timestomped = [] try: with open(mft_csv_path, "r", encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: si_created = row.get("Created0x10", "") fn_created = row.get("Created0x30", "") if si_created and fn_created: try: si_dt = datetime.fromisoformat(si_created.replace("Z", "+00:00")) fn_dt = datetime.fromisoformat(fn_created.replace("Z", "+00:00")) if si_dt < fn_dt: timestomped.append({ "file": row.get("FileName", "Unknown"), "entry_number": row.get("EntryNumber", ""), "si_created": si_created, "fn_created": fn_created, "indicator": "$SI Created before $FN Created" }) except (ValueError, TypeError): continue except FileNotFoundError: return [{"error": f"MFT CSV not found: {mft_csv_path}"}] return timestomped def process_all(self) -> dict: """Run all available EZ Tools processors against evidence.""" print("[*] Starting comprehensive EZ Tools processing...") print(f"[*] Evidence path: {self.evidence_path}") print(f"[*] Output path: {self.output_path}") available = self.verify_tools() print(f"[*] Available tools: {sum(v for v in available.values())}/{len(available)}") processors = [ ("MFT", self.process_mft), ("USN Journal", self.process_usn_journal), ("Prefetch", self.process_prefetch), ("LNK Files", self.process_lnk_files), ("Jump Lists", self.process_jump_lists), ("Event Logs", self.process_event_logs), ("Shellbags", self.process_shellbags), ("Recycle Bin", self.process_recycle_bin), ] for name, processor in processors: print(f"[*] Processing {name}...") result = processor() status = result.get("status", "unknown") print(f" [{status.upper()}] {name}") return self.results def generate_report(self) -> str: """Generate a summary report of all processing results.""" report = { "timestamp": datetime.now().isoformat(), "evidence_path": str(self.evidence_path), "output_path": str(self.output_path), "results": {} } for artifact, result in self.results.items(): report["results"][artifact] = { "status": result.get("status", "unknown"), "output_file": result.get("output_file", ""), } output_file = result.get("output_file", "") if output_file and os.path.exists(output_file): file_size = os.path.getsize(output_file) report["results"][artifact]["output_size_bytes"] = file_size with open(output_file, "rb") as f: report["results"][artifact]["sha256"] = hashlib.sha256(f.read()).hexdigest() report_path = self.output_path / "processing_report.json" with open(report_path, "w") as f: json.dump(report, f, indent=2) print(f"\n[*] Report saved to: {report_path}") return str(report_path) def main(): if len(sys.argv) < 4: print("Usage: python process.py ") print("Example: python process.py C:\\Tools\\EZTools C:\\Cases\\Evidence C:\\Cases\\Output") sys.exit(1) ez_tools_path = sys.argv[1] evidence_path = sys.argv[2] output_path = sys.argv[3] processor = EZToolsProcessor(ez_tools_path, evidence_path, output_path) processor.process_all() processor.generate_report() if __name__ == "__main__": main()