#!/usr/bin/env python3 """BigBrother — Network surveillance implant CLI. Main entry point: Click-based CLI + Rich interactive menu. Manages module lifecycle, status monitoring, and kill switch. """ import os import sys import time import signal import logging import inspect import importlib import json import sqlite3 from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Type import click from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.text import Text from rich.prompt import Prompt # Ensure project root is on sys.path PROJECT_ROOT = str(Path(__file__).resolve().parent) if PROJECT_ROOT not in sys.path: sys.path.insert(0, PROJECT_ROOT) from core.bus import EventBus from core.state import StateManager from core.engine import Engine, detect_hardware_tier, HARDWARE_TIERS from core.tool_manager import ToolManager from core.scheduler import Scheduler from core.resource_monitor import ResourceMonitor from core.kill_switch import KillSwitch from modules.base import BaseModule from utils.config_loader import load_config, ConfigLoader from utils.resource import ( detect_hardware, get_resource_usage, get_hardware_tier, get_cpu_temperature, get_disk_usage, ) console = Console() logger = logging.getLogger("bb.cli") # PID file for daemon mode PID_FILE = "/tmp/.bb.pid" # All module categories to discover MODULE_CATEGORIES = ["stealth", "passive", "active", "intel", "connectivity"] # --------------------------------------------------------------------------- # Module discovery # --------------------------------------------------------------------------- def discover_modules_in_category(category: str) -> Dict[str, Type[BaseModule]]: """Scan modules// for BaseModule subclasses.""" modules: Dict[str, Type[BaseModule]] = {} cat_dir = os.path.join(PROJECT_ROOT, "modules", category) if not os.path.isdir(cat_dir): return modules for filename in sorted(os.listdir(cat_dir)): if filename.startswith("_") or not filename.endswith(".py"): continue module_name = filename[:-3] dotted = f"modules.{category}.{module_name}" try: mod = importlib.import_module(dotted) except Exception as exc: logger.debug("Failed to import %s: %s", dotted, exc) continue for attr_name in dir(mod): obj = getattr(mod, attr_name) if ( isinstance(obj, type) and issubclass(obj, BaseModule) and obj is not BaseModule and hasattr(obj, "name") and obj.name != "unnamed" ): modules[obj.name] = obj return modules def discover_all_modules() -> Dict[str, Type[BaseModule]]: """Discover all modules across every category.""" all_modules: Dict[str, Type[BaseModule]] = {} for category in MODULE_CATEGORIES: all_modules.update(discover_modules_in_category(category)) return all_modules def discover_stealth_modules() -> Dict[str, Type[BaseModule]]: """Scan modules/stealth/ for BaseModule subclasses (backward compat).""" return discover_modules_in_category("stealth") def get_module_config(module_name: str, config: dict) -> dict: """Extract module-specific config from the master config.""" # Check for module-specific section in each category for category in MODULE_CATEGORIES: cat_cfg = config.get(category, {}) if module_name in cat_cfg: merged = dict(config) merged.update(cat_cfg[module_name]) return merged # Fallback: check top-level modules section mod_cfg = config.get("modules", {}).get(module_name, {}) if mod_cfg: merged = dict(config) merged.update(mod_cfg) return merged return dict(config) def is_module_enabled(module_name: str, module_type: str, config: dict) -> bool: """Check if a module is enabled in the config (modules.yaml).""" # Look in the category-level config cat_cfg = config.get(module_type, {}) if module_name in cat_cfg: return cat_cfg[module_name].get("enabled", True) # Default: stealth/passive/intel enabled, active disabled if module_type in ("stealth", "passive", "intel"): return True if module_type == "connectivity": # Connectivity has mixed defaults; check config return cat_cfg.get(module_name, {}).get("enabled", False) return False # active modules default to disabled # --------------------------------------------------------------------------- # Click CLI # --------------------------------------------------------------------------- @click.group(invoke_without_command=True) @click.pass_context def cli(ctx): """BigBrother — Network surveillance implant.""" ctx.ensure_object(dict) if ctx.invoked_subcommand is None: interactive_menu() @cli.command() @click.option("--daemon", is_flag=True, help="Fork to background, write PID file") @click.option("--passive-only", is_flag=True, help="Force passive-only engagement phase") def start(daemon, passive_only): """Start BigBrother — initialize all core systems and enabled modules.""" if daemon: _daemonize() console.print("[bold green]Starting BigBrother...[/bold green]") try: config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config")) except Exception as exc: console.print(f"[red]Config error:[/red] {exc}") sys.exit(1) if passive_only: config.setdefault("engagement_phase", {})["mode"] = "passive_only" # Initialize core components bus = EventBus() bus.start() state = StateManager() state.start() engine = Engine(bus=bus, state=state, config=config) tool_manager = ToolManager(bus=bus) tool_manager.start_monitoring() # Discover all modules across every category discovered = discover_all_modules() by_type: Dict[str, int] = {} for name, cls in discovered.items(): mod_type = getattr(cls, "module_type", "unknown") by_type[mod_type] = by_type.get(mod_type, 0) + 1 console.print(f" Discovered [cyan]{len(discovered)}[/cyan] modules: " + ", ".join(f"{t}={c}" for t, c in sorted(by_type.items()))) # Register enabled modules registered_count = 0 for name, cls in discovered.items(): mod_type = getattr(cls, "module_type", "unknown") if is_module_enabled(name, mod_type, config): mod_config = get_module_config(name, config) engine.register(cls, mod_config) registered_count += 1 console.print(f" Registered [cyan]{registered_count}[/cyan] enabled modules") # Start modules in dependency order results = engine.start_all() for name, success in results.items(): status_str = "[green]OK[/green]" if success else "[red]FAILED[/red]" console.print(f" {name}: {status_str}") # Start resource monitor res_mon = ResourceMonitor(bus=bus, state=state, config=config, engine=engine, tool_manager=tool_manager) res_mon.start() console.print(f"\n[bold green]BigBrother running[/bold green] (tier={engine.tier}, phase={engine.phase})") if daemon: # Write PID file with open(PID_FILE, "w") as f: f.write(str(os.getpid())) # Block main thread try: while True: time.sleep(60) except (KeyboardInterrupt, SystemExit): pass else: console.print("Press Ctrl+C to stop") try: while True: time.sleep(1) except KeyboardInterrupt: pass # Shutdown console.print("\n[yellow]Shutting down...[/yellow]") res_mon.stop() engine.stop_all() tool_manager.stop_all() state.stop() bus.stop() console.print("[green]BigBrother stopped.[/green]") @cli.command() def stop(): """Graceful shutdown — stop all modules in reverse dependency order.""" if os.path.exists(PID_FILE): try: with open(PID_FILE, "r") as f: pid = int(f.read().strip()) os.kill(pid, signal.SIGTERM) console.print(f"[green]Sent SIGTERM to BigBrother (PID {pid})[/green]") os.unlink(PID_FILE) except (ProcessLookupError, ValueError): console.print("[yellow]BigBrother not running (stale PID file)[/yellow]") os.unlink(PID_FILE) except PermissionError: console.print("[red]Permission denied — try with sudo[/red]") else: console.print("[yellow]No PID file found — BigBrother may not be running[/yellow]") @cli.command() def status(): """Show module status as a Rich table.""" try: config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config")) except Exception: config = {} state = StateManager() all_status = state.get_all_module_status() table = Table(title="BigBrother Module Status", show_lines=True) table.add_column("Module", style="cyan", min_width=20) table.add_column("Status", min_width=10) table.add_column("PID", justify="right", min_width=8) table.add_column("Uptime", justify="right", min_width=12) for name, info in sorted(all_status.items()): mod_status = info.get("status", "unknown") pid = str(info.get("pid", "-") or "-") started = info.get("started") if mod_status == "running": status_text = Text("running", style="bold green") if started: uptime_sec = time.time() - started uptime_str = _format_uptime(uptime_sec) else: uptime_str = "-" elif mod_status == "stopped": status_text = Text("stopped", style="dim") uptime_str = "-" elif mod_status == "error": status_text = Text("error", style="bold red") uptime_str = "-" else: status_text = Text(mod_status, style="yellow") uptime_str = "-" table.add_row(name, status_text, pid, uptime_str) if not all_status: table.add_row("[dim]No modules registered[/dim]", "", "", "") console.print(table) @cli.command() @click.argument("module_name") def activate(module_name): """Enable and start a specific module.""" try: config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config")) except Exception as exc: console.print(f"[red]Config error:[/red] {exc}") return discovered = discover_all_modules() if module_name not in discovered: console.print(f"[red]Module '{module_name}' not found.[/red]") console.print(f"Available: {', '.join(sorted(discovered.keys()))}") return bus = EventBus() bus.start() state = StateManager() state.start() engine = Engine(bus=bus, state=state, config=config) cls = discovered[module_name] mod_config = get_module_config(module_name, config) engine.register(cls, mod_config) success = engine.start(module_name) if success: console.print(f"[green]Module '{module_name}' activated.[/green]") else: console.print(f"[red]Failed to activate '{module_name}'.[/red]") @cli.command() @click.argument("module_name") def deactivate(module_name): """Stop and disable a specific module.""" state = StateManager() mod_status = state.get_module_status(module_name) if mod_status.get("status") == "unknown": console.print(f"[yellow]Module '{module_name}' is not registered.[/yellow]") return # Send SIGTERM to the module's PID if running pid = mod_status.get("pid") if pid: try: os.kill(pid, signal.SIGTERM) console.print(f"[green]Module '{module_name}' (PID {pid}) stopped.[/green]") except (ProcessLookupError, PermissionError) as exc: console.print(f"[yellow]Could not stop PID {pid}: {exc}[/yellow]") state.set_module_status(module_name, "stopped") console.print(f"[green]Module '{module_name}' deactivated.[/green]") @cli.command("kill") def kill_switch(): """Activate kill switch — full wipe and shutdown.""" console.print(Panel( "[bold red]KILL SWITCH[/bold red]\n\n" "This will:\n" " 1. Stop all modules and subprocesses\n" " 2. Send corrective ARP packets\n" " 3. Destroy LUKS encryption header\n" " 4. Shred all sensitive files\n" " 5. Zero-fill data partition\n" " 6. Clear RAM and reboot\n\n" "[bold]THIS ACTION IS IRREVERSIBLE.[/bold]", title="WARNING", border_style="red", )) confirm = Prompt.ask("Type WIPE to confirm", default="") if confirm != "WIPE": console.print("[yellow]Kill switch aborted.[/yellow]") return try: config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config")) except Exception: config = {} bus = EventBus() ks = KillSwitch(bus=bus, config=config) console.print("[red]Executing kill switch...[/red]") ks.execute(reason="manual_cli") @cli.command("config") def show_config(): """Pretty-print current configuration.""" try: config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config")) except Exception as exc: console.print(f"[red]Config error:[/red] {exc}") return from rich.syntax import Syntax import yaml yaml_str = yaml.dump(config, default_flow_style=False, sort_keys=False) syntax = Syntax(yaml_str, "yaml", theme="monokai", line_numbers=False) console.print(Panel(syntax, title="BigBrother Configuration", border_style="cyan")) @cli.command() def selftest(): """Run health checks: imports, tier detection, permissions, config, storage, binaries.""" console.print("[bold]Running self-test...[/bold]\n") passed = 0 failed = 0 # 1. Core imports tests = [ ("Core: EventBus", lambda: __import__("core.bus")), ("Core: StateManager", lambda: __import__("core.state")), ("Core: Engine", lambda: __import__("core.engine")), ("Core: ToolManager", lambda: __import__("core.tool_manager")), ("Core: KillSwitch", lambda: __import__("core.kill_switch")), ("Core: Scheduler", lambda: __import__("core.scheduler")), ("Core: ResourceMonitor", lambda: __import__("core.resource_monitor")), ("Utils: crypto", lambda: __import__("utils.crypto")), ("Utils: config_loader", lambda: __import__("utils.config_loader")), ("Utils: resource", lambda: __import__("utils.resource")), ] for label, test_fn in tests: try: test_fn() console.print(f" [green]PASS[/green] {label}") passed += 1 except Exception as exc: console.print(f" [red]FAIL[/red] {label}: {exc}") failed += 1 # 2. Hardware tier try: tier = get_hardware_tier() console.print(f" [green]PASS[/green] Hardware tier: {tier}") passed += 1 except Exception as exc: console.print(f" [red]FAIL[/red] Hardware tier: {exc}") failed += 1 # 3. Config loading try: config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config")) console.print(f" [green]PASS[/green] Config loaded ({len(config)} top-level keys)") passed += 1 except Exception as exc: console.print(f" [red]FAIL[/red] Config: {exc}") failed += 1 # 4. Permissions install_path = "/opt/.cache/bb" if os.path.isdir(install_path): console.print(f" [green]PASS[/green] Install path exists: {install_path}") passed += 1 else: console.print(f" [yellow]SKIP[/yellow] Install path not found: {install_path}") # 5. Storage try: total, free, used_pct = get_disk_usage("/") console.print(f" [green]PASS[/green] Disk: {total:.1f}GB total, {free:.1f}GB free ({used_pct:.1f}% used)") passed += 1 except Exception as exc: console.print(f" [red]FAIL[/red] Disk check: {exc}") failed += 1 # 6. External binaries binaries = ["tcpdump", "ip", "bettercap", "cryptsetup"] for binary in binaries: from shutil import which if which(binary): console.print(f" [green]PASS[/green] Binary: {binary}") passed += 1 else: console.print(f" [yellow]SKIP[/yellow] Binary not found: {binary}") # 7. Module discovery — all categories for category in MODULE_CATEGORIES: discovered = discover_modules_in_category(category) console.print(f" [green]PASS[/green] {category.capitalize()} modules: {len(discovered)}") passed += 1 console.print(f"\n[bold]Results: {passed} passed, {failed} failed[/bold]") @cli.command("modules") def list_modules(): """List all available modules with type, enabled status, and dependencies.""" try: config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config")) except Exception: config = {} discovered = discover_all_modules() table = Table(title="Available Modules", show_lines=True) table.add_column("Module", style="cyan", min_width=22) table.add_column("Type", min_width=12) table.add_column("Priority", justify="right", min_width=8) table.add_column("Root", justify="center", min_width=5) table.add_column("Enabled", justify="center", min_width=7) table.add_column("Dependencies", min_width=15) for name in sorted(discovered.keys()): cls = discovered[name] mod_type = getattr(cls, "module_type", "unknown") priority = str(getattr(cls, "priority", 0)) requires_root = "Yes" if getattr(cls, "requires_root", False) else "No" deps = ", ".join(getattr(cls, "dependencies", [])) or "-" enabled = is_module_enabled(name, mod_type, config) enabled_str = "[green]Yes[/green]" if enabled else "[dim]No[/dim]" table.add_row(name, mod_type, priority, requires_root, enabled_str, deps) console.print(table) console.print(f"\n Total: [cyan]{len(discovered)}[/cyan] modules across " f"{len(MODULE_CATEGORIES)} categories") # --------------------------------------------------------------------------- # Interactive menu — view functions # --------------------------------------------------------------------------- def _view_credentials(): """Query the credential database and display a Rich table.""" base_dir = os.path.expanduser("~/.bigbrother") db_path = os.path.join(base_dir, "credentials.db") if not os.path.isfile(db_path): console.print("\n[yellow]No credential database found.[/yellow]") console.print(f" Expected: {db_path}") console.print(" (Credentials are stored when modules capture them at runtime)") return try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row # Summary stats total = conn.execute("SELECT COUNT(*) FROM credentials").fetchone()[0] cracked = conn.execute( "SELECT COUNT(*) FROM credentials WHERE crack_status='cracked'" ).fetchone()[0] unique_users = conn.execute( "SELECT COUNT(DISTINCT username) FROM credentials" ).fetchone()[0] unique_hosts = conn.execute( "SELECT COUNT(DISTINCT target_ip) FROM credentials WHERE target_ip != ''" ).fetchone()[0] console.print(f"\n Total credentials: [cyan]{total}[/cyan] | " f"Cracked: [green]{cracked}[/green] | " f"Unique users: [yellow]{unique_users}[/yellow] | " f"Unique hosts: [magenta]{unique_hosts}[/magenta]") if total == 0: conn.close() return # Show recent credentials rows = conn.execute( """SELECT id, timestamp, source_module, target_ip, service, username, domain, cred_type, crack_status, cracked_value FROM credentials ORDER BY timestamp DESC LIMIT 50""" ).fetchall() table = Table(title="Credentials (most recent 50)", show_lines=True) table.add_column("ID", justify="right", min_width=4) table.add_column("Time", min_width=16) table.add_column("Source", min_width=14) table.add_column("Target", min_width=14) table.add_column("Service", min_width=10) table.add_column("Domain\\User", min_width=20) table.add_column("Type", min_width=10) table.add_column("Status", min_width=8) for row in rows: ts = datetime.fromtimestamp(row["timestamp"]).strftime("%Y-%m-%d %H:%M") domain_user = row["username"] if row["domain"]: domain_user = f"{row['domain']}\\{row['username']}" crack_status = row["crack_status"] if crack_status == "cracked": status_str = f"[green]cracked[/green]" elif crack_status == "cracking": status_str = f"[yellow]cracking[/yellow]" else: status_str = f"[dim]uncracked[/dim]" table.add_row( str(row["id"]), ts, row["source_module"] or "-", row["target_ip"] or "-", row["service"] or "-", domain_user, row["cred_type"] or "-", status_str, ) console.print(table) # Type breakdown type_rows = conn.execute( "SELECT cred_type, COUNT(*) as cnt FROM credentials GROUP BY cred_type ORDER BY cnt DESC" ).fetchall() if type_rows: console.print("\n By type: " + " | ".join( f"{r['cred_type']}={r['cnt']}" for r in type_rows )) conn.close() except Exception as exc: console.print(f"\n[red]Error reading credential database:[/red] {exc}") def _view_intel(): """Show an intelligence summary: hosts, credentials, modules, security tools.""" base_dir = os.path.expanduser("~/.bigbrother") state = StateManager() console.print("\n") intel_table = Table(title="Intelligence Summary", show_lines=True, min_width=60) intel_table.add_column("Category", style="cyan", min_width=25) intel_table.add_column("Value", min_width=30) # Host count from topology mapper state nodes_json = state.get("topology_mapper", "nodes") host_count = 0 if nodes_json: try: nodes = json.loads(nodes_json) host_count = len(nodes) except (json.JSONDecodeError, TypeError): pass intel_table.add_row("Discovered Hosts", str(host_count)) # Credential count cred_db_path = os.path.join(base_dir, "credentials.db") cred_count = 0 cracked_count = 0 if os.path.isfile(cred_db_path): try: conn = sqlite3.connect(cred_db_path) cred_count = conn.execute("SELECT COUNT(*) FROM credentials").fetchone()[0] cracked_count = conn.execute( "SELECT COUNT(*) FROM credentials WHERE crack_status='cracked'" ).fetchone()[0] conn.close() except Exception: pass intel_table.add_row("Credentials (total/cracked)", f"{cred_count} / {cracked_count}") # Active modules all_status = state.get_all_module_status() running_modules = [n for n, s in all_status.items() if s.get("status") == "running"] intel_table.add_row("Running Modules", str(len(running_modules))) if running_modules: intel_table.add_row(" Active", ", ".join(sorted(running_modules)[:10])) # Security posture risk_level = state.get("security_posture", "risk_level") if risk_level: risk_color = {"low": "green", "medium": "yellow", "high": "red", "critical": "bold red" }.get(risk_level, "dim") intel_table.add_row("Security Risk Level", f"[{risk_color}]{risk_level}[/{risk_color}]") else: intel_table.add_row("Security Risk Level", "[dim]not assessed[/dim]") has_edr = state.get("security_posture", "has_edr") if has_edr: intel_table.add_row("EDR Detected", "Yes" if json.loads(has_edr) else "No") has_siem = state.get("security_posture", "has_siem") if has_siem: intel_table.add_row("SIEM Detected", "Yes" if json.loads(has_siem) else "No") has_nac = state.get("security_posture", "has_nac") if has_nac: intel_table.add_row("NAC Detected", "Yes" if json.loads(has_nac) else "No") has_honeypot = state.get("security_posture", "has_honeypot") if has_honeypot: intel_table.add_row("Honeypots Detected", "Yes" if json.loads(has_honeypot) else "No") # VLANs vlans_json = state.get("topology_mapper", "vlans") if vlans_json: try: vlans = json.loads(vlans_json) intel_table.add_row("VLANs Discovered", str(len(vlans))) except (json.JSONDecodeError, TypeError): pass console.print(intel_table) def _view_topology(): """Show network topology from topology_mapper state data.""" state = StateManager() nodes_json = state.get("topology_mapper", "nodes") edges_json = state.get("topology_mapper", "edges") vlans_json = state.get("topology_mapper", "vlans") nodes = {} edges = {} vlans = {} if nodes_json: try: nodes = json.loads(nodes_json) except (json.JSONDecodeError, TypeError): pass if edges_json: try: edges = json.loads(edges_json) except (json.JSONDecodeError, TypeError): pass if vlans_json: try: vlans = json.loads(vlans_json) except (json.JSONDecodeError, TypeError): pass if not nodes: console.print("\n[yellow]No topology data available yet.[/yellow]") console.print(" Topology is built from passive host discovery and network observation.") return # Host table table = Table(title=f"Network Topology ({len(nodes)} hosts, {len(edges)} links, {len(vlans)} VLANs)", show_lines=True) table.add_column("IP", style="cyan", min_width=15) table.add_column("Hostname", min_width=18) table.add_column("MAC", min_width=17) table.add_column("OS", min_width=10) table.add_column("Role", min_width=12) table.add_column("VLAN", justify="right", min_width=5) table.add_column("Ports", min_width=15) for ip in sorted(nodes.keys()): node = nodes[ip] hostname = node.get("hostname", "") mac = node.get("mac", "") os_family = node.get("os_family", "unknown") role = node.get("role", "unknown") vlan = str(node.get("vlan", "")) if node.get("vlan") is not None else "-" ports = node.get("open_ports", []) if isinstance(ports, list): ports_str = ", ".join(str(p) for p in sorted(ports)[:8]) if len(ports) > 8: ports_str += f" (+{len(ports) - 8})" else: ports_str = str(ports) table.add_row(ip, hostname or "-", mac or "-", os_family, role, vlan, ports_str or "-") console.print(f"\n") console.print(table) # VLAN summary if vlans: console.print("\n VLANs:") for vid, vinfo in sorted(vlans.items(), key=lambda x: str(x[0])): name = vinfo.get("name", "") subnet = vinfo.get("subnet", "") console.print(f" VLAN {vid}: {name} ({subnet})" if name else f" VLAN {vid}: {subnet}") def _view_timeline(): """Show recent events from the operator audit log.""" base_dir = os.path.expanduser("~/.bigbrother") db_path = os.path.join(base_dir, "operator_audit.db") if not os.path.isfile(db_path): console.print("\n[yellow]No audit log found.[/yellow]") console.print(f" Expected: {db_path}") console.print(" (Audit log is created when operator_audit module starts)") return try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row total = conn.execute("SELECT COUNT(*) FROM audit_log").fetchone()[0] rows = conn.execute( """SELECT id, timestamp, action, operator, details FROM audit_log ORDER BY id DESC LIMIT 50""" ).fetchall() if not rows: console.print("\n[yellow]Audit log is empty.[/yellow]") conn.close() return console.print(f"\n Total audit entries: [cyan]{total}[/cyan] (showing most recent 50)") table = Table(title="Operator Timeline", show_lines=True) table.add_column("ID", justify="right", min_width=5) table.add_column("Time", min_width=19) table.add_column("Action", min_width=16) table.add_column("Operator", min_width=16) table.add_column("Details", min_width=30) for row in rows: ts = datetime.fromtimestamp(row["timestamp"]).strftime("%Y-%m-%d %H:%M:%S") action = row["action"] # Color code actions action_colors = { "module_start": "green", "module_stop": "yellow", "kill_switch": "bold red", "config_change": "magenta", "ssh_session": "cyan", "cli_command": "blue", "cred_export": "green", "data_exfil": "green", } color = action_colors.get(action, "dim") action_str = f"[{color}]{action}[/{color}]" table.add_row( str(row["id"]), ts, action_str, row["operator"] or "-", (row["details"] or "-")[:60], ) console.print(table) conn.close() except Exception as exc: console.print(f"\n[red]Error reading audit log:[/red] {exc}") # --------------------------------------------------------------------------- # Interactive menu # --------------------------------------------------------------------------- def interactive_menu(): """Rich-based numbered menu with hardware info and phase display.""" try: config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config")) except Exception: config = {} hw = detect_hardware() tier = hw.tier phase = config.get("engagement_phase", {}).get("mode", "passive_only") temp = get_cpu_temperature() temp_str = f"{temp:.1f}C" if temp is not None else "N/A" header = ( f"[bold cyan]BigBrother[/bold cyan] v1.0\n" f"Hardware: [yellow]{hw.model or 'Unknown'}[/yellow] ({tier})\n" f"CPU: {hw.cpu_cores} cores ({hw.architecture}) | RAM: {hw.total_ram_mb}MB | Temp: {temp_str}\n" f"Phase: [{'green' if phase == 'passive_only' else 'red'}]{phase}[/{'green' if phase == 'passive_only' else 'red'}]" ) while True: console.print() console.print(Panel(header, title="Dashboard", border_style="cyan")) console.print() console.print(" [bold]1.[/bold] Start all modules") console.print(" [bold]2.[/bold] Stop all modules") console.print(" [bold]3.[/bold] Module status") console.print(" [bold]4.[/bold] List modules") console.print(" [bold]5.[/bold] View Credentials") console.print(" [bold]6.[/bold] View Intelligence") console.print(" [bold]7.[/bold] Network Topology") console.print(" [bold]8.[/bold] Timeline") console.print(" [bold]9.[/bold] Self-test") console.print(" [bold]C.[/bold] Configuration") console.print(" [bold]K.[/bold] Kill switch") console.print(" [bold]Q.[/bold] Quit") console.print() choice = Prompt.ask("Select", default="Q") choice = choice.strip().upper() if choice == "1": ctx = click.Context(start) ctx.invoke(start, daemon=False, passive_only=False) elif choice == "2": ctx = click.Context(stop) ctx.invoke(stop) elif choice == "3": ctx = click.Context(status) ctx.invoke(status) elif choice == "4": ctx = click.Context(list_modules) ctx.invoke(list_modules) elif choice == "5": _view_credentials() elif choice == "6": _view_intel() elif choice == "7": _view_topology() elif choice == "8": _view_timeline() elif choice == "9": ctx = click.Context(selftest) ctx.invoke(selftest) elif choice == "C": ctx = click.Context(show_config) ctx.invoke(show_config) elif choice == "K": ctx = click.Context(kill_switch) ctx.invoke(kill_switch) elif choice == "Q": console.print("[dim]Goodbye.[/dim]") break else: console.print("[yellow]Invalid choice.[/yellow]") # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _format_uptime(seconds: float) -> str: """Format seconds into a human-readable uptime string.""" if seconds < 60: return f"{seconds:.0f}s" elif seconds < 3600: return f"{seconds / 60:.0f}m {seconds % 60:.0f}s" elif seconds < 86400: h = int(seconds // 3600) m = int((seconds % 3600) // 60) return f"{h}h {m}m" else: d = int(seconds // 86400) h = int((seconds % 86400) // 3600) return f"{d}d {h}h" def _daemonize(): """Fork into background, redirect stdio, write PID file.""" try: pid = os.fork() if pid > 0: # Parent exits sys.exit(0) except OSError as exc: console.print(f"[red]Fork failed:[/red] {exc}") sys.exit(1) # Decouple from parent os.setsid() os.umask(0) # Second fork try: pid = os.fork() if pid > 0: sys.exit(0) except OSError as exc: sys.exit(1) # Redirect stdio sys.stdout.flush() sys.stderr.flush() devnull = open(os.devnull, "r+b") os.dup2(devnull.fileno(), sys.stdin.fileno()) os.dup2(devnull.fileno(), sys.stdout.fileno()) os.dup2(devnull.fileno(), sys.stderr.fileno()) # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- if __name__ == "__main__": cli()