Add Phase 1 utility modules: crypto, networking, logging, stealth, resource, permissions, config_loader, bettercap_api
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
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user