#!/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 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" # --------------------------------------------------------------------------- # Module discovery # --------------------------------------------------------------------------- def discover_stealth_modules() -> Dict[str, Type[BaseModule]]: """Scan modules/stealth/ for BaseModule subclasses.""" modules: Dict[str, Type[BaseModule]] = {} stealth_dir = os.path.join(PROJECT_ROOT, "modules", "stealth") if not os.path.isdir(stealth_dir): return modules for filename in sorted(os.listdir(stealth_dir)): if filename.startswith("_") or not filename.endswith(".py"): continue module_name = filename[:-3] dotted = f"modules.stealth.{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 get_module_config(module_name: str, config: dict) -> dict: """Extract module-specific config from the master config.""" # Check for module-specific section mod_cfg = config.get("modules", {}).get(module_name, {}) if mod_cfg: # Merge with top-level config for access to device, network, etc. merged = dict(config) merged.update(mod_cfg) return merged return dict(config) # --------------------------------------------------------------------------- # 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 and register stealth modules discovered = discover_stealth_modules() console.print(f" Discovered [cyan]{len(discovered)}[/cyan] stealth modules") enabled_modules = config.get("modules", {}).get("enabled", list(discovered.keys())) for name, cls in discovered.items(): if name in enabled_modules or not config.get("modules", {}).get("enabled"): mod_config = get_module_config(name, config) engine.register(cls, mod_config) # 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_stealth_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. Stealth modules discovered = discover_stealth_modules() console.print(f" [green]PASS[/green] Stealth modules discovered: {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_stealth_modules() enabled_list = config.get("modules", {}).get("enabled") 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 "-" if enabled_list is None: enabled = "[green]Yes[/green]" elif name in enabled_list: enabled = "[green]Yes[/green]" else: enabled = "[dim]No[/dim]" table.add_row(name, mod_type, priority, requires_root, enabled, deps) console.print(table) # --------------------------------------------------------------------------- # 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] Credentials [dim](Available in Phase 2)[/dim]") console.print(" [bold]6.[/bold] Intelligence [dim](Available in Phase 2)[/dim]") console.print(" [bold]7.[/bold] Topology [dim](Available in Phase 2)[/dim]") console.print(" [bold]8.[/bold] Timeline [dim](Available in Phase 2)[/dim]") 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 in ("5", "6", "7", "8"): labels = {"5": "Credentials", "6": "Intelligence", "7": "Topology", "8": "Timeline"} console.print(f"\n[yellow]{labels[choice]} viewer is available in Phase 2.[/yellow]") 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()