62a1010ab4
9 files in utils/ providing the shared infrastructure layer: - crypto: AES-256-GCM file encryption, argon2id/PBKDF2 key derivation, HKDF network unlock, LUKS container management - networking: interface detection, MAC/IP helpers, BPF compilation via libpcap ctypes, gratuitous ARP, VLAN creation - logging: encrypted log writer (BBLogger) with rotation, per-module files, stdout suppression for stealth - stealth: process rename via prctl, cmdline spoofing, sysctl helpers, timestomping, core dump disable - resource: hardware tier detection (OPi Zero 3 / Pi Zero / generic) via /proc/device-tree and cpuinfo, resource monitoring - permissions: root/capability checks via capget ctypes, privilege dropping, directory permission enforcement - config_loader: YAML merge hierarchy (hardware_tiers -> bigbrother -> modules -> stealth -> CLI), tier auto-detection, SIGHUP reload - bettercap_api: REST client with per-session random credentials, session/events/command methods
180 lines
6.1 KiB
Python
180 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""YAML config loader with merge hierarchy, hardware tier detection, and SIGHUP reload."""
|
|
|
|
import copy
|
|
import os
|
|
import signal
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Dict, List, Optional
|
|
|
|
import yaml
|
|
|
|
from utils.resource import get_hardware_tier
|
|
|
|
CONFIG_DIR = "config"
|
|
MERGE_ORDER = [
|
|
"hardware_tiers.yaml",
|
|
"bigbrother.yaml",
|
|
"modules.yaml",
|
|
"stealth.yaml",
|
|
]
|
|
|
|
|
|
def deep_merge(base: dict, override: dict) -> dict:
|
|
result = copy.deepcopy(base)
|
|
for key, value in override.items():
|
|
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
result[key] = deep_merge(result[key], value)
|
|
else:
|
|
result[key] = copy.deepcopy(value)
|
|
return result
|
|
|
|
|
|
class ConfigLoader:
|
|
def __init__(
|
|
self,
|
|
config_dir: str = CONFIG_DIR,
|
|
cli_overrides: Optional[Dict[str, Any]] = None,
|
|
auto_detect_tier: bool = True,
|
|
):
|
|
self.config_dir = Path(config_dir)
|
|
self.cli_overrides = cli_overrides or {}
|
|
self.auto_detect_tier = auto_detect_tier
|
|
self._config: Dict[str, Any] = {}
|
|
self._lock = threading.Lock()
|
|
self._reload_callbacks: List[Callable] = []
|
|
self._loaded = False
|
|
|
|
def load(self) -> Dict[str, Any]:
|
|
with self._lock:
|
|
self._config = self._build_config()
|
|
self._loaded = True
|
|
return copy.deepcopy(self._config)
|
|
|
|
def _build_config(self) -> Dict[str, Any]:
|
|
merged: Dict[str, Any] = {}
|
|
|
|
for filename in MERGE_ORDER:
|
|
filepath = self.config_dir / filename
|
|
if filepath.exists():
|
|
try:
|
|
with open(filepath, "r") as f:
|
|
data = yaml.safe_load(f) or {}
|
|
merged = deep_merge(merged, data)
|
|
except (yaml.YAMLError, IOError):
|
|
continue
|
|
|
|
# Apply hardware tier defaults
|
|
if self.auto_detect_tier:
|
|
merged = self._apply_tier_limits(merged)
|
|
|
|
# Apply CLI overrides last (highest priority)
|
|
if self.cli_overrides:
|
|
merged = deep_merge(merged, self.cli_overrides)
|
|
|
|
# Validate
|
|
self._validate(merged)
|
|
|
|
return merged
|
|
|
|
def _apply_tier_limits(self, config: dict) -> dict:
|
|
tier = config.get("device", {}).get("platform", "auto")
|
|
if tier == "auto":
|
|
tier = get_hardware_tier()
|
|
config.setdefault("device", {})["platform"] = tier
|
|
|
|
# Extract tier-specific limits from hardware_tiers section
|
|
tiers = config.pop("hardware_tiers", {})
|
|
tier_config = tiers.get(tier, tiers.get("generic", {}))
|
|
|
|
if tier_config:
|
|
config = deep_merge(config, tier_config)
|
|
|
|
# Apply well-known tier constraints
|
|
if tier == "pi_zero":
|
|
config.setdefault("capture", {})["pcap_compression_level"] = 3
|
|
config.setdefault("security", {})["encryption_key_derive"] = "pbkdf2"
|
|
# Limit passive modules
|
|
limits = config.setdefault("limits", {})
|
|
limits.setdefault("max_passive_modules", 4)
|
|
limits.setdefault("max_active_modules", 0)
|
|
elif tier == "opi_zero3":
|
|
config.setdefault("capture", {})["pcap_compression_level"] = 19
|
|
config.setdefault("security", {})["encryption_key_derive"] = "argon2id"
|
|
limits = config.setdefault("limits", {})
|
|
limits.setdefault("max_passive_modules", 16)
|
|
limits.setdefault("max_active_modules", 6)
|
|
else:
|
|
config.setdefault("capture", {})["pcap_compression_level"] = 19
|
|
config.setdefault("security", {})["encryption_key_derive"] = "argon2id"
|
|
limits = config.setdefault("limits", {})
|
|
limits.setdefault("max_passive_modules", 16)
|
|
limits.setdefault("max_active_modules", -1) # unlimited
|
|
|
|
return config
|
|
|
|
def _validate(self, config: dict) -> None:
|
|
device = config.get("device", {})
|
|
platform = device.get("platform", "")
|
|
if platform and platform not in ("auto", "opi_zero3", "pi_zero", "generic"):
|
|
raise ValueError(f"Invalid platform: {platform}")
|
|
|
|
capture = config.get("capture", {})
|
|
comp_level = capture.get("pcap_compression_level")
|
|
if comp_level is not None and not (1 <= comp_level <= 22):
|
|
raise ValueError(f"Invalid compression level: {comp_level}")
|
|
|
|
bettercap = config.get("bettercap", {})
|
|
port = bettercap.get("api_port")
|
|
if port is not None and not (1 <= port <= 65535):
|
|
raise ValueError(f"Invalid bettercap API port: {port}")
|
|
|
|
security = config.get("security", {})
|
|
kdf = security.get("encryption_key_derive")
|
|
if kdf and kdf not in ("argon2id", "pbkdf2"):
|
|
raise ValueError(f"Invalid key derivation method: {kdf}")
|
|
|
|
def get(self, key_path: str, default: Any = None) -> Any:
|
|
with self._lock:
|
|
keys = key_path.split(".")
|
|
current = self._config
|
|
for k in keys:
|
|
if isinstance(current, dict) and k in current:
|
|
current = current[k]
|
|
else:
|
|
return default
|
|
return copy.deepcopy(current)
|
|
|
|
def reload(self) -> Dict[str, Any]:
|
|
config = self.load()
|
|
for cb in self._reload_callbacks:
|
|
try:
|
|
cb(config)
|
|
except Exception:
|
|
pass
|
|
return config
|
|
|
|
def on_reload(self, callback: Callable):
|
|
self._reload_callbacks.append(callback)
|
|
|
|
def register_sighup(self):
|
|
def _handler(signum, frame):
|
|
self.reload()
|
|
signal.signal(signal.SIGHUP, _handler)
|
|
|
|
@property
|
|
def config(self) -> Dict[str, Any]:
|
|
with self._lock:
|
|
if not self._loaded:
|
|
return self.load()
|
|
return copy.deepcopy(self._config)
|
|
|
|
|
|
def load_config(
|
|
config_dir: str = CONFIG_DIR,
|
|
cli_overrides: Optional[Dict[str, Any]] = None,
|
|
) -> Dict[str, Any]:
|
|
loader = ConfigLoader(config_dir=config_dir, cli_overrides=cli_overrides)
|
|
return loader.load()
|