Files
bigbrother/bigbrother.py
T
n0mad1k 3394c72814 Add main CLI entry point and full test suite
- bigbrother.py: Click CLI with start/stop/status/activate/deactivate/
  kill/config/selftest/modules commands plus Rich interactive menu.
  Module discovery scans modules/stealth/ for BaseModule subclasses.
  Supports --daemon mode with PID file and --passive-only flag.
- tests/conftest.py: Shared fixtures (tmp_dir, mock_config, mock_bus,
  mock_state, hardware_tier_override, project_root)
- tests/test_bus.py: 6 tests for EventBus pub/sub, filtering, wildcards
- tests/test_engine.py: 5 tests for Engine lifecycle, deps, tier, phase
- tests/test_crypto.py: 5 tests for AES-256-GCM, file encryption, KDF
- tests/test_resource.py: 7 tests for hardware detection, memory, disk
- tests/test_tool_manager.py: 6 tests for subprocess management
- tests/test_stealth_modules.py: 4 parametrized test classes covering
  all 12 stealth modules (import, inheritance, attributes, instantiation)
2026-03-18 09:48:23 -04:00

626 lines
21 KiB
Python
Executable File

#!/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()