Files
Cobra ffd384f64b Strip OPSEC tool identity fingerprints
Replace all sensor.* logger namespaces with __name__ (generic module
identifiers instead of discoverable 'sensor.*' prefixes).

Change hardcoded 'bb' API user to 'admin' in config and code defaults.

Change hardcoded relay_user 'bb' to 'operator' — prevents network
profiling from exposing tool identity via SSH config.

Fixes #457, #458, #459
2026-04-08 22:18:35 -04:00

1008 lines
34 KiB
Python
Executable File

#!/usr/bin/env python3
"""SystemMonitor — 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 core.capture_bus import CaptureBus
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(__name__)
# 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/<category>/ 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):
"""SystemMonitor — 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 SystemMonitor — initialize all core systems and enabled modules."""
if daemon:
_daemonize()
console.print("[bold green]Starting SystemMonitor...[/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]SystemMonitor 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(secure_wipe=True)
bus.stop()
console.print("[green]SystemMonitor 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 SystemMonitor (PID {pid})[/green]")
os.unlink(PID_FILE)
except (ProcessLookupError, ValueError):
console.print("[yellow]SystemMonitor 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 — SystemMonitor 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="SystemMonitor 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="SystemMonitor 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("~/.implant")
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("~/.implant")
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("~/.implant")
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]SystemMonitor[/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()