Add Phase 2 intel modules — 9 intelligence modules for on-device analysis
- credential_db: Central credential store with dedup, hashcat/CSV/JSON export, crack tracking - topology_mapper: Network graph from HOST_DISCOVERED/VLAN_DETECTED events, Graphviz DOT/SVG output - net_intel: Beacon detection (stddev/mean analysis), comm graph, DNS tunneling, service deps - user_timeline: Per-user activity timelines, work hours, admin/service account identification - supply_chain_detect: Passive detection of PyPI/npm mirrors, WSUS, CI/CD, SCCM, container registries - change_detector: Continuous baseline diff for burn detection, scan/security tool alerts - security_posture: EDR/SIEM/NAC/honeypot/scanner detection by DNS/port/UA, risk gating - operator_audit: Append-only HMAC chain audit log for engagement deconfliction - tool_output_parser: Unified parser for bettercap events, Responder logs, mitmproxy flows
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Operator audit trail — append-only log with HMAC integrity chain.
|
||||
|
||||
Logs all operator actions: SSH sessions, CLI commands, module activations,
|
||||
config changes. Each row includes an HMAC of the previous row, forming
|
||||
a tamper-evident chain. Subscribes to MODULE_STARTED, MODULE_STOPPED,
|
||||
and KILL_SWITCH events.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from modules.base import BaseModule
|
||||
|
||||
logger = logging.getLogger("bb.intel.operator_audit")
|
||||
|
||||
_SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp REAL NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
operator TEXT DEFAULT '',
|
||||
details TEXT DEFAULT '',
|
||||
hmac TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action);
|
||||
"""
|
||||
|
||||
# Action types
|
||||
ACTION_MODULE_START = "module_start"
|
||||
ACTION_MODULE_STOP = "module_stop"
|
||||
ACTION_KILL_SWITCH = "kill_switch"
|
||||
ACTION_CONFIG_CHANGE = "config_change"
|
||||
ACTION_SSH_SESSION = "ssh_session"
|
||||
ACTION_CLI_COMMAND = "cli_command"
|
||||
ACTION_CRED_EXPORT = "cred_export"
|
||||
ACTION_DATA_EXFIL = "data_exfil"
|
||||
ACTION_BASELINE_BUILD = "baseline_build"
|
||||
ACTION_MANUAL = "manual"
|
||||
|
||||
|
||||
class OperatorAudit(BaseModule):
|
||||
"""Append-only operator audit trail with HMAC integrity chain."""
|
||||
|
||||
name = "operator_audit"
|
||||
module_type = "intel"
|
||||
priority = 50 # Critical — always runs
|
||||
requires_root = False
|
||||
|
||||
def __init__(self, bus, state, config, engine=None):
|
||||
super().__init__(bus, state, config, engine)
|
||||
self._db_path = ""
|
||||
self._conn: Optional[sqlite3.Connection] = None
|
||||
self._lock = threading.Lock()
|
||||
self._hmac_key = b""
|
||||
self._last_hmac = ""
|
||||
self._entry_count = 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# BaseModule interface
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def start(self) -> None:
|
||||
if self._running:
|
||||
return
|
||||
|
||||
base_dir = self.config.get("data_dir", os.path.expanduser("~/.bigbrother"))
|
||||
self._db_path = os.path.join(base_dir, "operator_audit.db")
|
||||
os.makedirs(os.path.dirname(self._db_path), exist_ok=True)
|
||||
|
||||
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
||||
self._conn.execute("PRAGMA journal_mode=WAL")
|
||||
self._conn.execute("PRAGMA synchronous=NORMAL")
|
||||
self._conn.row_factory = sqlite3.Row
|
||||
self._conn.executescript(_SCHEMA)
|
||||
|
||||
# HMAC key: from config or derive from machine ID
|
||||
key_config = self.config.get("audit_hmac_key", "")
|
||||
if key_config:
|
||||
self._hmac_key = key_config.encode() if isinstance(key_config, str) else key_config
|
||||
else:
|
||||
self._hmac_key = self._derive_machine_key()
|
||||
|
||||
# Load last HMAC from chain
|
||||
self._last_hmac = self._get_last_hmac()
|
||||
|
||||
# Subscribe to events
|
||||
self.bus.subscribe(self._on_module_started, "MODULE_STARTED")
|
||||
self.bus.subscribe(self._on_module_stopped, "MODULE_STOPPED")
|
||||
self.bus.subscribe(self._on_kill_switch, "KILL_SWITCH")
|
||||
|
||||
self._running = True
|
||||
self._pid = os.getpid()
|
||||
self._start_time = time.time()
|
||||
self.state.set_module_status(self.name, "running", pid=self._pid)
|
||||
|
||||
# Log our own start
|
||||
self.log_action(
|
||||
ACTION_MODULE_START,
|
||||
details="OperatorAudit started",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"OperatorAudit started — db=%s, chain_length=%d",
|
||||
self._db_path, self._entry_count,
|
||||
)
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
# Log our own stop before shutting down
|
||||
self.log_action(
|
||||
ACTION_MODULE_STOP,
|
||||
details="OperatorAudit stopping",
|
||||
)
|
||||
|
||||
self._running = False
|
||||
|
||||
self.bus.unsubscribe(self._on_module_started, "MODULE_STARTED")
|
||||
self.bus.unsubscribe(self._on_module_stopped, "MODULE_STOPPED")
|
||||
self.bus.unsubscribe(self._on_kill_switch, "KILL_SWITCH")
|
||||
|
||||
if self._conn:
|
||||
self._conn.close()
|
||||
self._conn = None
|
||||
|
||||
self.state.set_module_status(self.name, "stopped")
|
||||
logger.info(
|
||||
"OperatorAudit stopped — %d entries in chain", self._entry_count
|
||||
)
|
||||
|
||||
def status(self) -> dict:
|
||||
return {
|
||||
"running": self._running,
|
||||
"pid": self._pid,
|
||||
"uptime": time.time() - self._start_time if self._start_time else 0,
|
||||
"entries": self._entry_count,
|
||||
"chain_valid": self.verify_chain(),
|
||||
}
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
old_config = json.dumps(self.config, sort_keys=True, default=str)
|
||||
self.config.update(config)
|
||||
new_config = json.dumps(self.config, sort_keys=True, default=str)
|
||||
|
||||
if old_config != new_config:
|
||||
self.log_action(
|
||||
ACTION_CONFIG_CHANGE,
|
||||
details=f"Config updated",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Event handlers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_module_started(self, event) -> None:
|
||||
p = event.payload
|
||||
module_name = p.get("module", event.source_module or "unknown")
|
||||
if module_name == self.name:
|
||||
return # Skip our own start event (already logged)
|
||||
self.log_action(
|
||||
ACTION_MODULE_START,
|
||||
details=f"Module started: {module_name} (pid={p.get('pid', '?')})",
|
||||
)
|
||||
|
||||
def _on_module_stopped(self, event) -> None:
|
||||
p = event.payload
|
||||
module_name = p.get("module", event.source_module or "unknown")
|
||||
if module_name == self.name:
|
||||
return
|
||||
self.log_action(
|
||||
ACTION_MODULE_STOP,
|
||||
details=f"Module stopped: {module_name}",
|
||||
)
|
||||
|
||||
def _on_kill_switch(self, event) -> None:
|
||||
p = event.payload
|
||||
self.log_action(
|
||||
ACTION_KILL_SWITCH,
|
||||
details=f"KILL SWITCH activated: reason={p.get('reason', 'unknown')}",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Core logging
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def log_action(self, action: str, operator: str = "",
|
||||
details: str = "") -> None:
|
||||
"""Log an operator action with HMAC chain integrity.
|
||||
|
||||
Args:
|
||||
action: Action type (use ACTION_* constants).
|
||||
operator: Operator identifier (SSH key fingerprint, username, etc).
|
||||
details: Free-text description of the action.
|
||||
"""
|
||||
if not self._conn:
|
||||
return
|
||||
|
||||
if not operator:
|
||||
operator = self._detect_operator()
|
||||
|
||||
ts = time.time()
|
||||
|
||||
# Compute HMAC: H(key, previous_hmac | timestamp | action | operator | details)
|
||||
msg = f"{self._last_hmac}|{ts}|{action}|{operator}|{details}"
|
||||
entry_hmac = hmac.new(
|
||||
self._hmac_key, msg.encode(), hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
with self._lock:
|
||||
try:
|
||||
self._conn.execute(
|
||||
"""INSERT INTO audit_log
|
||||
(timestamp, action, operator, details, hmac)
|
||||
VALUES (?, ?, ?, ?, ?)""",
|
||||
(ts, action, operator, details, entry_hmac),
|
||||
)
|
||||
self._conn.commit()
|
||||
self._last_hmac = entry_hmac
|
||||
self._entry_count += 1
|
||||
except Exception:
|
||||
logger.exception("Failed to log audit action: %s", action)
|
||||
|
||||
def log_ssh_session(self, remote_ip: str, key_fingerprint: str = "",
|
||||
event: str = "connect") -> None:
|
||||
"""Log an SSH session event."""
|
||||
self.log_action(
|
||||
ACTION_SSH_SESSION,
|
||||
operator=key_fingerprint or remote_ip,
|
||||
details=f"SSH {event} from {remote_ip} "
|
||||
f"(key={key_fingerprint or 'password'})",
|
||||
)
|
||||
|
||||
def log_cli_command(self, command: str, operator: str = "") -> None:
|
||||
"""Log a CLI command execution."""
|
||||
self.log_action(
|
||||
ACTION_CLI_COMMAND,
|
||||
operator=operator,
|
||||
details=f"Command: {command}",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# HMAC chain
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _derive_machine_key(self) -> bytes:
|
||||
"""Derive HMAC key from machine-specific identifiers."""
|
||||
components = []
|
||||
|
||||
# CPU serial (RPi)
|
||||
try:
|
||||
with open("/proc/cpuinfo", "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("Serial"):
|
||||
components.append(line.strip())
|
||||
break
|
||||
except (IOError, PermissionError):
|
||||
pass
|
||||
|
||||
# Machine ID
|
||||
try:
|
||||
with open("/etc/machine-id", "r") as f:
|
||||
components.append(f.read().strip())
|
||||
except (IOError, PermissionError):
|
||||
pass
|
||||
|
||||
if not components:
|
||||
components.append("bigbrother-default-key")
|
||||
|
||||
combined = "|".join(components)
|
||||
return hashlib.sha256(combined.encode()).digest()
|
||||
|
||||
def _get_last_hmac(self) -> str:
|
||||
"""Get the HMAC of the last entry in the chain."""
|
||||
with self._lock:
|
||||
row = self._conn.execute(
|
||||
"SELECT hmac FROM audit_log ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
|
||||
count_row = self._conn.execute(
|
||||
"SELECT COUNT(*) FROM audit_log"
|
||||
).fetchone()
|
||||
self._entry_count = count_row[0] if count_row else 0
|
||||
|
||||
if row:
|
||||
return row["hmac"]
|
||||
return "" # Genesis — empty for first entry
|
||||
|
||||
def _detect_operator(self) -> str:
|
||||
"""Attempt to detect the current operator identity."""
|
||||
# Check SSH connection
|
||||
ssh_client = os.environ.get("SSH_CLIENT", "")
|
||||
if ssh_client:
|
||||
parts = ssh_client.split()
|
||||
remote_ip = parts[0] if parts else "unknown"
|
||||
|
||||
# Try to get SSH key fingerprint
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["who", "-m"],
|
||||
capture_output=True, timeout=2,
|
||||
)
|
||||
who = result.stdout.decode().strip()
|
||||
if who:
|
||||
return f"ssh:{remote_ip}:{who.split()[0]}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return f"ssh:{remote_ip}"
|
||||
|
||||
# Local operator
|
||||
user = os.environ.get("USER", os.environ.get("LOGNAME", "local"))
|
||||
return f"local:{user}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Chain verification
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def verify_chain(self) -> bool:
|
||||
"""Verify the entire HMAC chain integrity.
|
||||
|
||||
Returns:
|
||||
True if the chain is valid, False if tampered.
|
||||
"""
|
||||
if not self._conn:
|
||||
return False
|
||||
|
||||
with self._lock:
|
||||
rows = self._conn.execute(
|
||||
"SELECT timestamp, action, operator, details, hmac "
|
||||
"FROM audit_log ORDER BY id ASC"
|
||||
).fetchall()
|
||||
|
||||
if not rows:
|
||||
return True # Empty chain is valid
|
||||
|
||||
prev_hmac = ""
|
||||
for row in rows:
|
||||
ts = row["timestamp"]
|
||||
action = row["action"]
|
||||
operator = row["operator"]
|
||||
details = row["details"]
|
||||
stored_hmac = row["hmac"]
|
||||
|
||||
msg = f"{prev_hmac}|{ts}|{action}|{operator}|{details}"
|
||||
computed = hmac.new(
|
||||
self._hmac_key, msg.encode(), hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
if computed != stored_hmac:
|
||||
logger.error(
|
||||
"HMAC chain broken at ts=%.3f action=%s — "
|
||||
"expected %s, got %s",
|
||||
ts, action, computed[:16], stored_hmac[:16],
|
||||
)
|
||||
return False
|
||||
|
||||
prev_hmac = stored_hmac
|
||||
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Export
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def export_log(self, since: float = 0, limit: int = 10000) -> list:
|
||||
"""Export audit log entries for engagement reports."""
|
||||
with self._lock:
|
||||
rows = self._conn.execute(
|
||||
"""SELECT * FROM audit_log
|
||||
WHERE timestamp > ?
|
||||
ORDER BY id ASC LIMIT ?""",
|
||||
(since, limit),
|
||||
).fetchall()
|
||||
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def export_json(self, since: float = 0) -> str:
|
||||
"""Export audit log as JSON string."""
|
||||
entries = self.export_log(since)
|
||||
return json.dumps(entries, indent=2, default=str)
|
||||
|
||||
def get_summary(self) -> dict:
|
||||
"""Return audit log summary."""
|
||||
with self._lock:
|
||||
rows = self._conn.execute(
|
||||
"""SELECT action, COUNT(*) FROM audit_log
|
||||
GROUP BY action ORDER BY COUNT(*) DESC"""
|
||||
).fetchall()
|
||||
|
||||
operators = self._conn.execute(
|
||||
"""SELECT DISTINCT operator FROM audit_log
|
||||
WHERE operator != ''"""
|
||||
).fetchall()
|
||||
|
||||
return {
|
||||
"total_entries": self._entry_count,
|
||||
"chain_valid": self.verify_chain(),
|
||||
"by_action": {a: c for a, c in rows},
|
||||
"operators": [r[0] for r in operators],
|
||||
}
|
||||
Reference in New Issue
Block a user