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
260 lines
7.1 KiB
Python
260 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Hardware tier detection and resource monitoring via /proc and /sys."""
|
|
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
TIER_OPI_ZERO3 = "opi_zero3"
|
|
TIER_PI_ZERO = "pi_zero"
|
|
TIER_GENERIC = "generic"
|
|
|
|
|
|
@dataclass
|
|
class HardwareInfo:
|
|
tier: str
|
|
model: str
|
|
cpu_model: str
|
|
cpu_cores: int
|
|
total_ram_mb: int
|
|
architecture: str
|
|
|
|
|
|
@dataclass
|
|
class ResourceUsage:
|
|
ram_used_mb: float
|
|
ram_total_mb: float
|
|
ram_percent: float
|
|
cpu_percent: float # System-wide, from /proc/stat delta
|
|
disk_used_pct: float
|
|
disk_free_mb: float
|
|
temperature_c: Optional[float]
|
|
|
|
|
|
@dataclass
|
|
class ProcessResources:
|
|
pid: int
|
|
name: str
|
|
rss_mb: float
|
|
vms_mb: float
|
|
cpu_ticks: int
|
|
state: str
|
|
threads: int
|
|
|
|
|
|
def get_hardware_tier() -> str:
|
|
info = detect_hardware()
|
|
return info.tier
|
|
|
|
|
|
def detect_hardware() -> HardwareInfo:
|
|
model = _read_device_model()
|
|
cpu_model, cpu_cores, arch = _read_cpuinfo()
|
|
|
|
if "Orange Pi Zero3" in model or "Orange Pi Zero 3" in model:
|
|
tier = TIER_OPI_ZERO3
|
|
elif "Raspberry Pi Zero 2" in model or "Raspberry Pi Zero2" in model:
|
|
tier = TIER_PI_ZERO
|
|
elif "Raspberry Pi" in model and ("Zero" in model or "zero" in model):
|
|
tier = TIER_PI_ZERO
|
|
elif "Allwinner" in cpu_model or "H618" in cpu_model:
|
|
# Fallback detection via CPU for OPi Zero 3
|
|
tier = TIER_OPI_ZERO3
|
|
elif "BCM2835" in cpu_model or "BCM2710" in cpu_model:
|
|
tier = TIER_PI_ZERO
|
|
else:
|
|
tier = TIER_GENERIC
|
|
|
|
total_ram = _read_total_ram_mb()
|
|
|
|
return HardwareInfo(
|
|
tier=tier,
|
|
model=model or "Unknown",
|
|
cpu_model=cpu_model,
|
|
cpu_cores=cpu_cores,
|
|
total_ram_mb=total_ram,
|
|
architecture=arch,
|
|
)
|
|
|
|
|
|
def _read_device_model() -> str:
|
|
dt_model = Path("/proc/device-tree/model")
|
|
if dt_model.exists():
|
|
try:
|
|
return dt_model.read_text().strip().rstrip("\x00")
|
|
except IOError:
|
|
pass
|
|
return ""
|
|
|
|
|
|
def _read_cpuinfo() -> Tuple[str, int, str]:
|
|
cpu_model = ""
|
|
cores = 0
|
|
arch = "unknown"
|
|
|
|
try:
|
|
with open("/proc/cpuinfo", "r") as f:
|
|
content = f.read()
|
|
|
|
for line in content.splitlines():
|
|
if line.startswith("model name") or line.startswith("Hardware"):
|
|
cpu_model = line.split(":", 1)[1].strip()
|
|
if line.startswith("processor"):
|
|
cores += 1
|
|
|
|
# Get architecture
|
|
with open("/proc/cpuinfo", "r") as f:
|
|
first_lines = f.read(4096)
|
|
if "aarch64" in first_lines.lower() or "ARMv8" in first_lines:
|
|
arch = "aarch64"
|
|
elif "armv7" in first_lines.lower():
|
|
arch = "armv7l"
|
|
else:
|
|
import platform
|
|
arch = platform.machine()
|
|
except IOError:
|
|
pass
|
|
|
|
return cpu_model, max(cores, 1), arch
|
|
|
|
|
|
def _read_total_ram_mb() -> int:
|
|
try:
|
|
with open("/proc/meminfo", "r") as f:
|
|
for line in f:
|
|
if line.startswith("MemTotal:"):
|
|
kb = int(line.split()[1])
|
|
return kb // 1024
|
|
except IOError:
|
|
pass
|
|
return 0
|
|
|
|
|
|
def get_memory_info() -> Dict[str, int]:
|
|
info = {}
|
|
try:
|
|
with open("/proc/meminfo", "r") as f:
|
|
for line in f:
|
|
parts = line.split()
|
|
key = parts[0].rstrip(":")
|
|
val = int(parts[1]) # in kB
|
|
info[key] = val
|
|
except IOError:
|
|
pass
|
|
return info
|
|
|
|
|
|
def get_resource_usage(disk_path: str = "/") -> ResourceUsage:
|
|
mem = get_memory_info()
|
|
total = mem.get("MemTotal", 0)
|
|
available = mem.get("MemAvailable", mem.get("MemFree", 0))
|
|
used = total - available
|
|
pct = (used / total * 100) if total > 0 else 0.0
|
|
|
|
stat = os.statvfs(disk_path)
|
|
disk_total = stat.f_blocks * stat.f_frsize
|
|
disk_free = stat.f_bavail * stat.f_frsize
|
|
disk_used_pct = ((disk_total - disk_free) / disk_total * 100) if disk_total > 0 else 0.0
|
|
|
|
temp = get_cpu_temperature()
|
|
|
|
return ResourceUsage(
|
|
ram_used_mb=used / 1024,
|
|
ram_total_mb=total / 1024,
|
|
ram_percent=pct,
|
|
cpu_percent=_get_cpu_percent(),
|
|
disk_used_pct=disk_used_pct,
|
|
disk_free_mb=disk_free / (1024 * 1024),
|
|
temperature_c=temp,
|
|
)
|
|
|
|
|
|
def get_cpu_temperature() -> Optional[float]:
|
|
thermal_zones = Path("/sys/class/thermal")
|
|
if thermal_zones.exists():
|
|
for zone in sorted(thermal_zones.iterdir()):
|
|
temp_file = zone / "temp"
|
|
type_file = zone / "type"
|
|
if temp_file.exists():
|
|
try:
|
|
temp_raw = int(temp_file.read_text().strip())
|
|
return temp_raw / 1000.0
|
|
except (IOError, ValueError):
|
|
continue
|
|
# Fallback for some SBCs
|
|
hwmon = Path("/sys/class/hwmon")
|
|
if hwmon.exists():
|
|
for hw in sorted(hwmon.iterdir()):
|
|
temp_file = hw / "temp1_input"
|
|
if temp_file.exists():
|
|
try:
|
|
return int(temp_file.read_text().strip()) / 1000.0
|
|
except (IOError, ValueError):
|
|
continue
|
|
return None
|
|
|
|
|
|
def _get_cpu_percent() -> float:
|
|
try:
|
|
with open("/proc/stat", "r") as f:
|
|
line = f.readline()
|
|
parts = line.split()
|
|
# user, nice, system, idle, iowait, irq, softirq, steal
|
|
total = sum(int(p) for p in parts[1:8])
|
|
idle = int(parts[4])
|
|
# Snapshot-based; for delta, caller should sample twice
|
|
if total > 0:
|
|
return ((total - idle) / total) * 100
|
|
except (IOError, IndexError, ValueError):
|
|
pass
|
|
return 0.0
|
|
|
|
|
|
def get_process_resources(pid: int) -> Optional[ProcessResources]:
|
|
try:
|
|
stat_path = f"/proc/{pid}/stat"
|
|
status_path = f"/proc/{pid}/status"
|
|
|
|
with open(stat_path, "r") as f:
|
|
stat_line = f.read()
|
|
|
|
# Parse /proc/PID/stat — comm field can contain spaces/parens
|
|
match = re.match(r"(\d+) \((.+)\) (.+)", stat_line)
|
|
if not match:
|
|
return None
|
|
|
|
name = match.group(2)
|
|
fields = match.group(3).split()
|
|
state = fields[0]
|
|
utime = int(fields[11])
|
|
stime = int(fields[12])
|
|
threads = int(fields[17])
|
|
vsize = int(fields[20])
|
|
rss_pages = int(fields[21])
|
|
|
|
page_size = os.sysconf("SC_PAGE_SIZE")
|
|
rss_mb = (rss_pages * page_size) / (1024 * 1024)
|
|
vms_mb = vsize / (1024 * 1024)
|
|
|
|
return ProcessResources(
|
|
pid=pid,
|
|
name=name,
|
|
rss_mb=rss_mb,
|
|
vms_mb=vms_mb,
|
|
cpu_ticks=utime + stime,
|
|
state=state,
|
|
threads=threads,
|
|
)
|
|
except (IOError, OSError, IndexError, ValueError):
|
|
return None
|
|
|
|
|
|
def get_disk_usage(path: str = "/") -> Tuple[float, float, float]:
|
|
stat = os.statvfs(path)
|
|
total_gb = (stat.f_blocks * stat.f_frsize) / (1024 ** 3)
|
|
free_gb = (stat.f_bavail * stat.f_frsize) / (1024 ** 3)
|
|
used_pct = ((stat.f_blocks - stat.f_bavail) / stat.f_blocks * 100) if stat.f_blocks > 0 else 0
|
|
return total_gb, free_gb, used_pct
|