Phase 5: Final integration — fix imports, full module discovery, interactive menu

- Fix pyserial import error in cellular_backup.py (lazy import with fallback)
- Replace stealth-only module discovery with discover_all_modules() across all
  5 categories (stealth, passive, active, intel, connectivity)
- Wire up interactive menu options 5-8:
  - View Credentials: query credential_db SQLite, Rich table with stats
  - View Intelligence: host count, cred stats, security posture summary
  - Network Topology: host table from topology_mapper state data
  - Timeline: recent entries from operator_audit HMAC-chained log
- Update start command to discover+register all enabled modules (not just stealth)
- Update modules/activate/selftest commands to use full module discovery
- Add is_module_enabled() config-aware helper for per-category defaults
- Fix modules.yaml module count headers (16->17 passive, 8->9 active/intel)
- Set bettercap_mgr enabled=false (all active modules disabled by default)
- Add tests/test_all_modules.py: 179 parametrized tests covering import,
  BaseModule subclass, attributes, and instantiation for all 55 modules
  across passive, active, intel, and connectivity categories
- All 256 tests passing
This commit is contained in:
n0mad1k
2026-03-18 13:59:27 -04:00
parent ba5143b560
commit 56cca8ac3d
4 changed files with 847 additions and 40 deletions
+416 -35
View File
@@ -12,6 +12,9 @@ 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
@@ -50,25 +53,28 @@ 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_stealth_modules() -> Dict[str, Type[BaseModule]]:
"""Scan modules/stealth/ for BaseModule subclasses."""
def discover_modules_in_category(category: str) -> Dict[str, Type[BaseModule]]:
"""Scan modules/<category>/ for BaseModule subclasses."""
modules: Dict[str, Type[BaseModule]] = {}
stealth_dir = os.path.join(PROJECT_ROOT, "modules", "stealth")
cat_dir = os.path.join(PROJECT_ROOT, "modules", category)
if not os.path.isdir(stealth_dir):
if not os.path.isdir(cat_dir):
return modules
for filename in sorted(os.listdir(stealth_dir)):
for filename in sorted(os.listdir(cat_dir)):
if filename.startswith("_") or not filename.endswith(".py"):
continue
module_name = filename[:-3]
dotted = f"modules.stealth.{module_name}"
dotted = f"modules.{category}.{module_name}"
try:
mod = importlib.import_module(dotted)
@@ -90,18 +96,53 @@ def discover_stealth_modules() -> Dict[str, Type[BaseModule]]:
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
# 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:
# Merge with top-level config for access to device, network, etc.
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
# ---------------------------------------------------------------------------
@@ -145,15 +186,26 @@ def start(daemon, passive_only):
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()))
# Discover all modules across every category
discovered = discover_all_modules()
by_type: Dict[str, int] = {}
for name, cls in discovered.items():
if name in enabled_modules or not config.get("modules", {}).get("enabled"):
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()
@@ -272,7 +324,7 @@ def activate(module_name):
console.print(f"[red]Config error:[/red] {exc}")
return
discovered = discover_stealth_modules()
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()))}")
@@ -443,10 +495,11 @@ def selftest():
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
# 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]")
@@ -459,8 +512,7 @@ def list_modules():
except Exception:
config = {}
discovered = discover_stealth_modules()
enabled_list = config.get("modules", {}).get("enabled")
discovered = discover_all_modules()
table = Table(title="Available Modules", show_lines=True)
table.add_column("Module", style="cyan", min_width=22)
@@ -477,16 +529,340 @@ def list_modules():
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]"
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, deps)
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}")
# ---------------------------------------------------------------------------
@@ -521,10 +897,10 @@ def interactive_menu():
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]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")
@@ -546,9 +922,14 @@ def interactive_menu():
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 == "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)