Files
bigbrother/scripts/operator/generate_report.py
T
n0mad1k ba5143b560 Phase 4: Active modules, templates, and operator scripts
Active modules (9 files in modules/active/):
- bettercap_mgr: Central bettercap orchestrator with REST API health
  monitoring, event stream parsing, crash recovery with corrective
  gratuitous ARPs, caplet management, and process disguise
- arp_spoof: Thin bettercap wrapper for ARP spoofing with OPSEC warnings
- dns_poison: DNS poisoning with zone template loading support
- dhcp_spoof: DHCPv6 spoofing via bettercap for rogue DNS injection
- evil_twin: hostapd-based rogue AP with captive portal and dnsmasq,
  iptables redirect, credential capture via HTTP POST handler
- ipv6_slaac: IPv6 SLAAC spoofing via bettercap + mitm6 WPAD abuse
- responder_mgr: Responder subprocess manager with hash file monitoring,
  NTLMv1/v2 parsing, session log scanning, relay target coordination
- mitmproxy_mgr: Transparent proxy with addon scripts, tier checking
  (OPi Zero 3+ only), iptables setup, credential/token extraction
- ntlm_relay: ntlmrelayx wrapper with multi-protocol relay (SMB, LDAP,
  LDAPS, HTTP, MSSQL, ADCS), Responder exclusion coordination, SOCKS

Templates (9 files):
- 4 captive portals: corporate SSO, guest WiFi, Outlook/M365, VPN
  (self-contained HTML with inline CSS, realistic login forms)
- 2 DNS zones: redirect-all and selective Jinja2 template
- 2 hostapd configs: open AP and WPA2-PSK Jinja2 templates
- 1 Responder.conf Jinja2 template with protocol toggles

Operator scripts (6 files in scripts/operator/):
- pull_data.sh: rsync structured data over WireGuard/Tailscale
- extract_files.sh: tshark HTTP/SMB/FTP/TFTP file extraction
- extract_print_jobs.sh: TCP/9100 print job reconstruction + PDF convert
- extract_emails.sh: SMTP email extraction with attachment detection
- crack_hashes.sh: Export creds to hashcat format, optional auto-crack
- generate_report.py: SQLite-to-Markdown/HTML engagement report generator
2026-03-18 13:48:11 -04:00

510 lines
17 KiB
Python
Executable File

#!/usr/bin/env python3
"""BigBrother Operator Script — Generate Engagement Report.
Pulls data from SQLite databases synced from the implant and generates
a structured engagement report in both Markdown and HTML formats.
Sections:
1. Executive Summary
2. Network Topology & Host Inventory
3. Captured Credentials
4. DNS Intelligence
5. Traffic Analysis
6. Timeline of Events
7. Recommendations
Usage:
python3 generate_report.py <data_dir> [--output <path>] [--title <title>]
Examples:
python3 generate_report.py ./bb-pull-20240115
python3 generate_report.py ./bb-pull-20240115 --title "ACME Corp Assessment"
"""
import argparse
import json
import os
import sqlite3
import sys
import time
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
# ---------------------------------------------------------------------------
# Database helpers
# ---------------------------------------------------------------------------
def connect_db(db_path):
"""Connect to a SQLite database if it exists."""
if not os.path.isfile(db_path):
return None
try:
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
except Exception:
return None
def safe_query(conn, sql, params=None):
"""Execute a query, returning empty list on error."""
if conn is None:
return []
try:
cursor = conn.execute(sql, params or ())
return cursor.fetchall()
except Exception:
return []
# ---------------------------------------------------------------------------
# Data collection
# ---------------------------------------------------------------------------
def collect_credentials(data_dir):
"""Collect credentials from credential database and Responder logs."""
creds = []
# From credential DB
conn = connect_db(os.path.join(data_dir, "databases", "credentials.db"))
if conn:
rows = safe_query(conn, """
SELECT timestamp, source_ip, target_ip, target_service,
username, domain, credential_type, hashcat_mode
FROM credentials
ORDER BY timestamp
""")
for r in rows:
creds.append({
"timestamp": r["timestamp"],
"source_ip": r["source_ip"] or "",
"target_ip": r["target_ip"] or "",
"service": r["target_service"] or "",
"username": r["username"] or "",
"domain": r["domain"] or "",
"type": r["credential_type"] or "",
"hashcat_mode": r["hashcat_mode"],
})
conn.close()
return creds
def collect_hosts(data_dir):
"""Collect host inventory from state database."""
hosts = []
conn = connect_db(os.path.join(data_dir, "databases", "state.db"))
if conn:
rows = safe_query(conn, """
SELECT key, value FROM kv_store
WHERE module = 'host_discovery'
""")
for r in rows:
try:
host_data = json.loads(r["value"])
if isinstance(host_data, dict):
hosts.append(host_data)
except (json.JSONDecodeError, TypeError):
pass
conn.close()
# Also check topology DB
conn = connect_db(os.path.join(data_dir, "databases", "topology.db"))
if conn:
rows = safe_query(conn, """
SELECT ip, mac, hostname, os, vendor, first_seen, last_seen
FROM hosts
ORDER BY ip
""")
for r in rows:
hosts.append({
"ip": r["ip"],
"mac": r["mac"] or "",
"hostname": r["hostname"] or "",
"os": r["os"] or "",
"vendor": r["vendor"] or "",
"first_seen": r["first_seen"],
"last_seen": r["last_seen"],
})
conn.close()
return hosts
def collect_dns_stats(data_dir):
"""Collect DNS query statistics."""
stats = {"total_queries": 0, "top_domains": [], "top_queriers": [], "doh_count": 0}
conn = connect_db(os.path.join(data_dir, "databases", "dns_queries.db"))
if not conn:
return stats
# Total queries
rows = safe_query(conn, "SELECT COUNT(*) as cnt FROM dns_queries")
stats["total_queries"] = rows[0]["cnt"] if rows else 0
# DoH detections
rows = safe_query(conn, "SELECT COUNT(*) as cnt FROM dns_queries WHERE is_doh = 1")
stats["doh_count"] = rows[0]["cnt"] if rows else 0
# Top queried domains
rows = safe_query(conn, """
SELECT domain, COUNT(*) as cnt
FROM dns_queries WHERE is_doh = 0
GROUP BY domain ORDER BY cnt DESC LIMIT 20
""")
stats["top_domains"] = [(r["domain"], r["cnt"]) for r in rows]
# Top querier IPs
rows = safe_query(conn, """
SELECT source_ip, COUNT(*) as cnt
FROM dns_queries WHERE is_doh = 0
GROUP BY source_ip ORDER BY cnt DESC LIMIT 15
""")
stats["top_queriers"] = [(r["source_ip"], r["cnt"]) for r in rows]
conn.close()
return stats
def collect_module_status(data_dir):
"""Collect module runtime status."""
modules = {}
conn = connect_db(os.path.join(data_dir, "databases", "state.db"))
if conn:
rows = safe_query(conn, """
SELECT module, status, started, updated
FROM module_status
""")
for r in rows:
modules[r["module"]] = {
"status": r["status"],
"started": r["started"],
"updated": r["updated"],
}
conn.close()
return modules
# ---------------------------------------------------------------------------
# Report generation
# ---------------------------------------------------------------------------
def generate_markdown(data_dir, title, creds, hosts, dns_stats, modules):
"""Generate Markdown report."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
lines = []
def add(text=""):
lines.append(text)
# Header
add(f"# {title}")
add(f"\n**Generated:** {now}")
add(f"**Data Source:** `{os.path.abspath(data_dir)}`\n")
add("---\n")
# Executive Summary
add("## 1. Executive Summary\n")
add(f"- **Hosts Discovered:** {len(hosts)}")
add(f"- **Credentials Captured:** {len(creds)}")
cred_types = Counter(c["type"] for c in creds)
for ctype, count in cred_types.most_common():
add(f" - {ctype}: {count}")
unique_users = len(set(c["username"] for c in creds if c["username"]))
add(f"- **Unique Users:** {unique_users}")
add(f"- **DNS Queries Logged:** {dns_stats['total_queries']:,}")
add(f"- **DoH Blind Spots:** {dns_stats['doh_count']}")
add(f"- **Modules Active:** {sum(1 for m in modules.values() if m['status'] == 'running')}")
add("")
# Host Inventory
add("## 2. Network Topology & Host Inventory\n")
if hosts:
add("| IP | MAC | Hostname | OS | Vendor |")
add("|---|---|---|---|---|")
for h in hosts[:100]: # Cap at 100 for readability
add(f"| {h.get('ip', '')} | {h.get('mac', '')} | {h.get('hostname', '')} "
f"| {h.get('os', '')} | {h.get('vendor', '')} |")
if len(hosts) > 100:
add(f"\n*({len(hosts)} total hosts — showing first 100)*\n")
else:
add("*No host data available.*\n")
# Credentials
add("\n## 3. Captured Credentials\n")
if creds:
add("| Time | User | Domain | Service | Type |")
add("|---|---|---|---|---|")
for c in creds[:50]:
ts = ""
if c.get("timestamp"):
try:
ts = datetime.fromtimestamp(c["timestamp"], tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
except Exception:
pass
add(f"| {ts} | {c['username']} | {c['domain']} | {c['service']} | {c['type']} |")
if len(creds) > 50:
add(f"\n*({len(creds)} total credentials — showing first 50)*\n")
add("\n### Credential Summary by Type\n")
for ctype, count in cred_types.most_common():
add(f"- **{ctype}**: {count}")
add("\n### Unique Users by Service\n")
service_users = defaultdict(set)
for c in creds:
if c["username"]:
service_users[c["service"]].add(c["username"])
for svc, users in sorted(service_users.items()):
add(f"- **{svc}**: {', '.join(sorted(users)[:10])}"
+ (f" (+{len(users)-10} more)" if len(users) > 10 else ""))
else:
add("*No credentials captured.*\n")
# DNS Intelligence
add("\n## 4. DNS Intelligence\n")
add(f"- **Total Queries:** {dns_stats['total_queries']:,}")
add(f"- **DoH Detections (blind spots):** {dns_stats['doh_count']}\n")
if dns_stats["top_domains"]:
add("### Top Queried Domains\n")
add("| Domain | Queries |")
add("|---|---|")
for domain, count in dns_stats["top_domains"]:
add(f"| {domain} | {count:,} |")
if dns_stats["top_queriers"]:
add("\n### Top DNS Clients\n")
add("| IP | Queries |")
add("|---|---|")
for ip, count in dns_stats["top_queriers"]:
add(f"| {ip} | {count:,} |")
# Module Status
add("\n## 5. Module Status\n")
if modules:
add("| Module | Status | Started |")
add("|---|---|---|")
for name, info in sorted(modules.items()):
started = ""
if info.get("started"):
try:
started = datetime.fromtimestamp(
info["started"], tz=timezone.utc
).strftime("%Y-%m-%d %H:%M")
except Exception:
pass
add(f"| {name} | {info['status']} | {started} |")
else:
add("*No module status data available.*\n")
# Recommendations
add("\n## 6. Recommendations\n")
add("1. **Credential Analysis**: Run `crack_hashes.sh` against captured NTLMv2 hashes")
add("2. **File Extraction**: Run `extract_files.sh` on PCAPs for document recovery")
add("3. **Print Jobs**: Check for print traffic with `extract_print_jobs.sh`")
add("4. **Email Analysis**: Run `extract_emails.sh` for SMTP traffic recovery")
add("5. **Offline Analysis**: Run Zeek against PCAPs for deep protocol analysis")
add("")
add("---\n")
add(f"*Report generated by BigBrother operator tooling — {now}*")
return "\n".join(lines)
def markdown_to_html(markdown_text, title):
"""Convert Markdown report to standalone HTML."""
# Simple Markdown-to-HTML conversion (no external deps)
html_lines = []
in_table = False
in_list = False
html_lines.append(f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:1000px;margin:0 auto;padding:40px 20px;color:#1d1d1f;line-height:1.6;background:#fafafa}}
h1{{color:#1a1a2e;border-bottom:3px solid #0066cc;padding-bottom:12px}}
h2{{color:#16213e;margin-top:32px;border-bottom:1px solid #ddd;padding-bottom:8px}}
h3{{color:#333;margin-top:20px}}
table{{border-collapse:collapse;width:100%;margin:12px 0;font-size:14px}}
th,td{{border:1px solid #ddd;padding:8px 12px;text-align:left}}
th{{background:#f0f2f5;font-weight:600}}
tr:nth-child(even){{background:#f9f9f9}}
tr:hover{{background:#f0f2f5}}
code{{background:#f0f2f5;padding:2px 6px;border-radius:4px;font-size:13px}}
hr{{border:none;border-top:1px solid #ddd;margin:24px 0}}
ul{{margin:8px 0}}
li{{margin:4px 0}}
strong{{color:#1a1a2e}}
em{{color:#666}}
</style>
</head>
<body>
""")
for line in markdown_text.split("\n"):
stripped = line.strip()
# Headings
if stripped.startswith("# "):
if in_table:
html_lines.append("</table>")
in_table = False
level = len(stripped) - len(stripped.lstrip("#"))
text = stripped.lstrip("# ").strip()
html_lines.append(f"<h{level}>{_inline_format(text)}</h{level}>")
continue
# Horizontal rule
if stripped == "---":
if in_table:
html_lines.append("</table>")
in_table = False
html_lines.append("<hr>")
continue
# Table
if "|" in stripped and stripped.startswith("|"):
cells = [c.strip() for c in stripped.split("|")[1:-1]]
if all(c.replace("-", "").replace(":", "") == "" for c in cells):
continue # Skip separator row
if not in_table:
html_lines.append("<table>")
in_table = True
tag = "th"
else:
tag = "td"
row = "".join(f"<{tag}>{_inline_format(c)}</{tag}>" for c in cells)
html_lines.append(f"<tr>{row}</tr>")
continue
if in_table and not stripped.startswith("|"):
html_lines.append("</table>")
in_table = False
# List items
if stripped.startswith("- ") or stripped.startswith("* "):
if not in_list:
html_lines.append("<ul>")
in_list = True
text = stripped[2:].strip()
html_lines.append(f"<li>{_inline_format(text)}</li>")
continue
elif stripped.startswith(tuple(f"{i}. " for i in range(1, 20))):
if not in_list:
html_lines.append("<ol>")
in_list = True
text = stripped.split(". ", 1)[1] if ". " in stripped else stripped
html_lines.append(f"<li>{_inline_format(text)}</li>")
continue
if in_list and not stripped.startswith(("-", "*")) and not stripped[:2].rstrip(".").isdigit():
html_lines.append("</ul>" if in_list else "</ol>")
in_list = False
# Empty line
if not stripped:
continue
# Paragraph
html_lines.append(f"<p>{_inline_format(stripped)}</p>")
if in_table:
html_lines.append("</table>")
if in_list:
html_lines.append("</ul>")
html_lines.append("</body></html>")
return "\n".join(html_lines)
def _inline_format(text):
"""Apply inline Markdown formatting (bold, code, italic)."""
import re
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
text = re.sub(r"`(.+?)`", r"<code>\1</code>", text)
text = re.sub(r"\*(.+?)\*", r"<em>\1</em>", text)
return text
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Generate BigBrother engagement report from synced data."
)
parser.add_argument("data_dir", help="Path to pulled data directory")
parser.add_argument("--output", "-o", help="Output directory (default: <data_dir>/report)")
parser.add_argument("--title", "-t", default="BigBrother Engagement Report",
help="Report title")
args = parser.parse_args()
data_dir = args.data_dir
output_dir = args.output or os.path.join(data_dir, "report")
if not os.path.isdir(data_dir):
print(f"[-] Data directory not found: {data_dir}", file=sys.stderr)
sys.exit(1)
os.makedirs(output_dir, exist_ok=True)
print(f"[*] Generating report: {args.title}")
print(f" Data: {os.path.abspath(data_dir)}")
print(f" Output: {os.path.abspath(output_dir)}")
print()
# Collect data
print("[*] Collecting credentials...")
creds = collect_credentials(data_dir)
print(f" Found {len(creds)} credentials")
print("[*] Collecting host inventory...")
hosts = collect_hosts(data_dir)
print(f" Found {len(hosts)} hosts")
print("[*] Collecting DNS statistics...")
dns_stats = collect_dns_stats(data_dir)
print(f" {dns_stats['total_queries']:,} queries logged")
print("[*] Collecting module status...")
modules = collect_module_status(data_dir)
print(f" {len(modules)} modules tracked")
print()
# Generate Markdown
print("[*] Generating Markdown report...")
md_report = generate_markdown(data_dir, args.title, creds, hosts, dns_stats, modules)
md_path = os.path.join(output_dir, "report.md")
with open(md_path, "w") as f:
f.write(md_report)
print(f" Saved: {md_path}")
# Generate HTML
print("[*] Generating HTML report...")
html_report = markdown_to_html(md_report, args.title)
html_path = os.path.join(output_dir, "report.html")
with open(html_path, "w") as f:
f.write(html_report)
print(f" Saved: {html_path}")
print()
print(f"[+] Report generation complete")
print(f" Markdown: {md_path}")
print(f" HTML: {html_path}")
if __name__ == "__main__":
main()