ffd384f64b
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
1008 lines
34 KiB
Python
Executable File
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()
|