#!/usr/bin/env python3 """Browser history artifact extraction agent using sqlite3 for Chrome/Firefox/Edge forensics.""" import argparse import csv import json import logging import os import sqlite3 from datetime import datetime, timedelta from typing import List logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) CHROME_EPOCH = datetime(1601, 1, 1) UNIX_EPOCH = datetime(1970, 1, 1) def chrome_time_to_utc(chrome_ts: int) -> str: """Convert Chrome/WebKit timestamp (microseconds since 1601-01-01) to ISO UTC.""" if not chrome_ts or chrome_ts < 0: return "" try: dt = CHROME_EPOCH + timedelta(microseconds=chrome_ts) return dt.isoformat() + "Z" except (OverflowError, ValueError): return "" def firefox_time_to_utc(ff_ts: int) -> str: """Convert Firefox timestamp (microseconds since Unix epoch) to ISO UTC.""" if not ff_ts or ff_ts < 0: return "" try: dt = UNIX_EPOCH + timedelta(microseconds=ff_ts) return dt.isoformat() + "Z" except (OverflowError, ValueError): return "" def extract_chrome_history(db_path: str, limit: int = 5000) -> List[dict]: """Extract browsing history from Chrome/Edge History database.""" if not os.path.exists(db_path): logger.warning("Chrome History DB not found: %s", db_path) return [] conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) cursor = conn.cursor() cursor.execute(""" SELECT urls.url, urls.title, urls.last_visit_time, urls.visit_count, urls.typed_count FROM urls ORDER BY urls.last_visit_time DESC LIMIT ? """, (limit,)) rows = cursor.fetchall() conn.close() results = [] for url, title, last_visit, visit_count, typed_count in rows: results.append({ "url": url, "title": title or "", "last_visit": chrome_time_to_utc(last_visit), "visit_count": visit_count, "typed_count": typed_count, }) logger.info("Extracted %d Chrome history entries from %s", len(results), db_path) return results def extract_chrome_downloads(db_path: str, limit: int = 1000) -> List[dict]: """Extract downloads from Chrome/Edge History database.""" if not os.path.exists(db_path): return [] conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) cursor = conn.cursor() cursor.execute(""" SELECT current_path, tab_url, total_bytes, start_time, end_time, mime_type, danger_type FROM downloads ORDER BY start_time DESC LIMIT ? """, (limit,)) rows = cursor.fetchall() conn.close() return [{ "path": r[0], "source_url": r[1], "size_bytes": r[2], "start_time": chrome_time_to_utc(r[3]), "end_time": chrome_time_to_utc(r[4]), "mime_type": r[5], "danger_type": r[6], } for r in rows] def extract_chrome_cookies(db_path: str, limit: int = 5000) -> List[dict]: """Extract cookies from Chrome Cookies database.""" if not os.path.exists(db_path): return [] conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) cursor = conn.cursor() cursor.execute(""" SELECT host_key, name, path, creation_utc, last_access_utc, is_secure, is_httponly FROM cookies ORDER BY last_access_utc DESC LIMIT ? """, (limit,)) rows = cursor.fetchall() conn.close() return [{ "host": r[0], "name": r[1], "path": r[2], "created": chrome_time_to_utc(r[3]), "last_access": chrome_time_to_utc(r[4]), "secure": bool(r[5]), "httponly": bool(r[6]), } for r in rows] def extract_firefox_history(db_path: str, limit: int = 5000) -> List[dict]: """Extract browsing history from Firefox places.sqlite.""" if not os.path.exists(db_path): logger.warning("Firefox places.sqlite not found: %s", db_path) return [] conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) cursor = conn.cursor() cursor.execute(""" SELECT moz_places.url, moz_places.title, moz_historyvisits.visit_date, moz_places.visit_count, moz_historyvisits.visit_type FROM moz_places JOIN moz_historyvisits ON moz_places.id = moz_historyvisits.place_id ORDER BY moz_historyvisits.visit_date DESC LIMIT ? """, (limit,)) rows = cursor.fetchall() conn.close() return [{ "url": r[0], "title": r[1] or "", "visit_date": firefox_time_to_utc(r[2]), "visit_count": r[3], "visit_type": r[4], } for r in rows] def extract_firefox_cookies(db_path: str, limit: int = 5000) -> List[dict]: """Extract cookies from Firefox cookies.sqlite.""" if not os.path.exists(db_path): return [] conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) cursor = conn.cursor() cursor.execute(""" SELECT host, name, path, creationTime, lastAccessed, isSecure, isHttpOnly FROM moz_cookies ORDER BY lastAccessed DESC LIMIT ? """, (limit,)) rows = cursor.fetchall() conn.close() return [{ "host": r[0], "name": r[1], "path": r[2], "created": firefox_time_to_utc(r[3]), "last_access": firefox_time_to_utc(r[4]), "secure": bool(r[5]), "httponly": bool(r[6]), } for r in rows] def export_to_csv(data: List[dict], output_path: str) -> None: """Export extracted data to CSV.""" if not data: return with open(output_path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=data[0].keys()) writer.writeheader() writer.writerows(data) logger.info("Exported %d rows to %s", len(data), output_path) def generate_report(chrome_dir: str = "", firefox_dir: str = "", output_dir: str = ".") -> dict: """Generate comprehensive browser forensics report.""" report = {"analysis_date": datetime.utcnow().isoformat(), "browsers": {}} if chrome_dir and os.path.isdir(chrome_dir): history = extract_chrome_history(os.path.join(chrome_dir, "History")) downloads = extract_chrome_downloads(os.path.join(chrome_dir, "History")) cookies = extract_chrome_cookies(os.path.join(chrome_dir, "Cookies")) report["browsers"]["chrome"] = { "history_count": len(history), "download_count": len(downloads), "cookie_count": len(cookies), } export_to_csv(history, os.path.join(output_dir, "chrome_history.csv")) export_to_csv(downloads, os.path.join(output_dir, "chrome_downloads.csv")) if firefox_dir and os.path.isdir(firefox_dir): history = extract_firefox_history(os.path.join(firefox_dir, "places.sqlite")) cookies = extract_firefox_cookies(os.path.join(firefox_dir, "cookies.sqlite")) report["browsers"]["firefox"] = { "history_count": len(history), "cookie_count": len(cookies), } export_to_csv(history, os.path.join(output_dir, "firefox_history.csv")) return report def main(): parser = argparse.ArgumentParser(description="Browser History Extraction Agent") parser.add_argument("--chrome-dir", default="", help="Path to Chrome/Edge User Data/Default") parser.add_argument("--firefox-dir", default="", help="Path to Firefox profile directory") parser.add_argument("--output-dir", default=".", help="Output directory for CSVs and report") parser.add_argument("--output", default="browser_report.json") args = parser.parse_args() os.makedirs(args.output_dir, exist_ok=True) report = generate_report(args.chrome_dir, args.firefox_dir, args.output_dir) with open(os.path.join(args.output_dir, args.output), "w") as f: json.dump(report, f, indent=2) logger.info("Report saved") print(json.dumps(report, indent=2)) if __name__ == "__main__": main()