Fix DB table/column mismatch and stale PID noise on startup

build_data.py created 'profiles' but mac_manager queries 'mac_profiles'.
Columns category/model renamed to device_type/device_name. device_type
values aligned with _CATEGORY_MAP in mac_manager (streaming, smart_tv,
phone, tablet, smart_speaker, iot, printer, gaming).

ja3_fingerprints.db table renamed from 'fingerprints' to 'ja3_profiles'
and expanded with all columns ja3_spoofer queries (name, description,
cipher_suites, extensions, elliptic_curves, ec_point_formats as JSON).

StateManager.reset_module_status() clears stale 'running' entries on
startup so watchdog doesn't flag old-session PIDs as dead modules.
Engine.start_all() calls reset before launch and does a 3s post-startup
liveness check, logging any modules that crashed immediately after fork.
This commit is contained in:
Cobra
2026-04-07 13:05:31 -04:00
parent e15e077be8
commit 0f754cfb7f
3 changed files with 152 additions and 50 deletions
+32 -3
View File
@@ -363,8 +363,12 @@ class Engine:
def start_all(self) -> dict[str, bool]: def start_all(self) -> dict[str, bool]:
"""Start all registered modules in dependency order.""" """Start all registered modules in dependency order."""
# Clear stale PIDs from any previous run so the watchdog does not
# report modules from the old session as dead.
self.state.reset_module_status()
order = _topo_sort(self._modules) order = _topo_sort(self._modules)
# Instantiate and start CaptureBus for passive modules # Instantiate and start CaptureBus for passive modules
iface = self.config.get("network", {}).get("primary_interface") iface = self.config.get("network", {}).get("primary_interface")
if iface: if iface:
@@ -376,17 +380,42 @@ class Engine:
except Exception as e: except Exception as e:
logger.error("Failed to start CaptureBus: %s", e) logger.error("Failed to start CaptureBus: %s", e)
self.capture_bus = None self.capture_bus = None
# Inject capture_bus into module configs for passive modules # Inject capture_bus into module configs for passive modules
# Some modules use "capture_bus", others "_capture_bus" — inject both # Some modules use "capture_bus", others "_capture_bus" — inject both
for name, entry in self._modules.items(): for name, entry in self._modules.items():
if getattr(entry.module_class, "requires_capture_bus", False): if getattr(entry.module_class, "requires_capture_bus", False):
entry.config["capture_bus"] = self.capture_bus entry.config["capture_bus"] = self.capture_bus
entry.config["_capture_bus"] = self.capture_bus entry.config["_capture_bus"] = self.capture_bus
results = {} results = {}
for name in order: for name in order:
results[name] = self.start(name) results[name] = self.start(name)
# Post-startup liveness check: wait for module processes to settle,
# then verify each one is still alive and log a clear summary.
time.sleep(3)
alive, dead = [], []
with self._lock:
for name, entry in self._modules.items():
if results.get(name):
if entry.process and entry.process.is_alive():
alive.append(name)
else:
dead.append(name)
# Update state so watchdog doesn't double-report
self.state.set_module_status(name, "stopped")
if dead:
logger.error(
"Startup check: %d modules died immediately after launch: %s",
len(dead), ", ".join(dead),
)
logger.info(
"Startup check complete — running: %d, failed: %d",
len(alive), len(dead),
)
return results return results
def stop_all(self, timeout: float = 5.0) -> None: def stop_all(self, timeout: float = 5.0) -> None:
+22
View File
@@ -232,6 +232,28 @@ class StateManager:
for r in rows for r in rows
} }
def reset_module_status(self) -> None:
"""Mark all running modules as stopped.
Called at engine startup to clear stale PIDs from a previous run.
Without this, the watchdog picks up old PIDs and reports dead modules
that were never started in the current session.
"""
conn = self._get_write_conn()
try:
conn.execute("BEGIN IMMEDIATE")
conn.execute(
"UPDATE module_status SET status='stopped', pid=NULL, updated=? "
"WHERE status='running'",
(time.time(),),
)
conn.commit()
except Exception:
try:
conn.rollback()
except Exception:
pass
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Write queue # Write queue
# ------------------------------------------------------------------ # ------------------------------------------------------------------
+98 -47
View File
@@ -21,19 +21,22 @@ from pathlib import Path
def create_innocuous_macs_db(db_path: str) -> None: def create_innocuous_macs_db(db_path: str) -> None:
"""Create innocuous MAC profiles database with ~50 real device profiles. """Create innocuous MAC profiles database with ~50 real device profiles.
Each profile includes OUI, vendor, model, DHCP hostname pattern, Each profile includes OUI, vendor, device_name, DHCP hostname pattern,
DHCP vendor class, TTL, TCP window size, and OS family -- everything DHCP vendor class, TTL, TCP window size, and OS family -- everything
needed to impersonate the device at the network fingerprint level. needed to impersonate the device at the network fingerprint level.
Table name and column names match what mac_manager.py queries:
mac_profiles(device_type, device_name, vendor, oui, ...)
""" """
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA journal_mode=WAL")
conn.executescript(""" conn.executescript("""
CREATE TABLE IF NOT EXISTS profiles ( CREATE TABLE IF NOT EXISTS mac_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL, device_type TEXT NOT NULL,
oui TEXT NOT NULL, oui TEXT NOT NULL,
vendor TEXT NOT NULL, vendor TEXT NOT NULL,
model TEXT NOT NULL, device_name TEXT NOT NULL,
dhcp_hostname TEXT, dhcp_hostname TEXT,
dhcp_vendor_class TEXT, dhcp_vendor_class TEXT,
ttl INTEGER DEFAULT 64, ttl INTEGER DEFAULT 64,
@@ -41,8 +44,8 @@ def create_innocuous_macs_db(db_path: str) -> None:
os_family TEXT DEFAULT 'Linux' os_family TEXT DEFAULT 'Linux'
); );
CREATE INDEX IF NOT EXISTS idx_profiles_category ON profiles(category); CREATE INDEX IF NOT EXISTS idx_mac_profiles_type ON mac_profiles(device_type);
CREATE INDEX IF NOT EXISTS idx_profiles_oui ON profiles(oui); CREATE INDEX IF NOT EXISTS idx_mac_profiles_oui ON mac_profiles(oui);
""") """)
profiles = [ profiles = [
@@ -83,17 +86,17 @@ def create_innocuous_macs_db(db_path: str) -> None:
"Sony-TV", "Sony", 64, 65535, "Android"), "Sony-TV", "Sony", 64, 65535, "Android"),
# ── Smart speakers / assistants ──────────────────────────────── # ── Smart speakers / assistants ────────────────────────────────
("speaker", "48:A6:B8", "Sonos", "Sonos One", ("smart_speaker", "48:A6:B8", "Sonos", "Sonos One",
"Sonos-", "Sonos", 64, 65535, "Linux"), "Sonos-", "Sonos", 64, 65535, "Linux"),
("speaker", "5C:AA:FD", "Sonos", "Sonos Beam", ("smart_speaker", "5C:AA:FD", "Sonos", "Sonos Beam",
"Sonos-", "Sonos", 64, 65535, "Linux"), "Sonos-", "Sonos", 64, 65535, "Linux"),
("speaker", "30:FD:38", "Google", "Google Home Mini", ("smart_speaker", "30:FD:38", "Google", "Google Home Mini",
"Google-Home-", "Google", 64, 65535, "Linux"), "Google-Home-", "Google", 64, 65535, "Linux"),
("speaker", "1C:F2:9A", "Google", "Google Nest Hub", ("smart_speaker", "1C:F2:9A", "Google", "Google Nest Hub",
"Google-Nest-", "Google", 64, 65535, "Linux"), "Google-Nest-", "Google", 64, 65535, "Linux"),
("speaker", "68:54:FD", "Amazon", "Echo Dot", ("smart_speaker", "68:54:FD", "Amazon", "Echo Dot",
"amazon-", "AmazonEcho", 64, 26883, "Android"), "amazon-", "AmazonEcho", 64, 26883, "Android"),
("speaker", "74:C2:46", "Amazon", "Echo Show", ("smart_speaker", "74:C2:46", "Amazon", "Echo Show",
"amazon-", "AmazonEcho", 64, 26883, "Android"), "amazon-", "AmazonEcho", 64, 26883, "Android"),
# ── Smart home / IoT ────────────────────────────────────────── # ── Smart home / IoT ──────────────────────────────────────────
@@ -148,30 +151,40 @@ def create_innocuous_macs_db(db_path: str) -> None:
("gaming", "7C:BB:8A", "Nintendo", "Nintendo Switch", ("gaming", "7C:BB:8A", "Nintendo", "Nintendo Switch",
"Nintendo-", "Nintendo", 64, 65535, "Nintendo"), "Nintendo-", "Nintendo", 64, 65535, "Nintendo"),
# ── Phones / tablets ──────────────────────────────────────────
("phone", "A4:83:E7", "Apple", "iPhone 15 Pro",
"iphone", "dhcpcd-6.x.x", 64, 65535, "iOS"),
("phone", "40:CB:C0", "Apple", "iPad Air",
"ipad", "dhcpcd-6.x.x", 64, 65535, "iPadOS"),
("tablet", "3C:28:6D", "Samsung", "Galaxy Tab S9",
"Samsung-Tab", "dhcpcd-6.x.x", 64, 65535, "Android"),
("phone", "DC:1A:C5", "Samsung", "Galaxy S24",
"Galaxy-S", "dhcpcd-6.x.x", 64, 65535, "Android"),
# ── Network gear (hide as infrastructure) ───────────────────── # ── Network gear (hide as infrastructure) ─────────────────────
("network", "B4:FB:E4", "Ubiquiti", "UniFi AP", ("iot", "B4:FB:E4", "Ubiquiti", "UniFi AP",
"UAP-", "Ubiquiti", 64, 65535, "Linux"), "UAP-", "Ubiquiti", 64, 65535, "Linux"),
("network", "78:8A:20", "Ubiquiti", "USG Gateway", ("iot", "78:8A:20", "Ubiquiti", "USG Gateway",
"USG-", "Ubiquiti", 64, 65535, "Linux"), "USG-", "Ubiquiti", 64, 65535, "Linux"),
("network", "14:CC:20", "TP-Link", "TP-Link Archer AX50", ("iot", "14:CC:20", "TP-Link", "TP-Link Archer AX50",
"TL-", "TP-Link", 64, 65535, "Linux"), "TL-", "TP-Link", 64, 65535, "Linux"),
("network", "AC:84:C6", "TP-Link", "TP-Link Deco M5", ("iot", "AC:84:C6", "TP-Link", "TP-Link Deco M5",
"Deco-", "TP-Link", 64, 65535, "Linux"), "Deco-", "TP-Link", 64, 65535, "Linux"),
("network", "20:A6:CD", "Netgear", "Netgear Orbi", ("iot", "20:A6:CD", "Netgear", "Netgear Orbi",
"Orbi-", "Netgear", 64, 65535, "Linux"), "Orbi-", "Netgear", 64, 65535, "Linux"),
("network", "C4:04:15", "Netgear", "Netgear Nighthawk", ("iot", "C4:04:15", "Netgear", "Netgear Nighthawk",
"NETGEAR-", "Netgear", 64, 65535, "Linux"), "NETGEAR-", "Netgear", 64, 65535, "Linux"),
] ]
conn.executemany(""" conn.executemany("""
INSERT INTO profiles (category, oui, vendor, model, INSERT INTO mac_profiles (device_type, oui, vendor, device_name,
dhcp_hostname, dhcp_vendor_class, dhcp_hostname, dhcp_vendor_class,
ttl, tcp_window, os_family) ttl, tcp_window, os_family)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", profiles) """, profiles)
conn.commit() conn.commit()
count = conn.execute("SELECT COUNT(*) FROM profiles").fetchone()[0] count = conn.execute("SELECT COUNT(*) FROM mac_profiles").fetchone()[0]
conn.close() conn.close()
print(f" innocuous_macs.db: {count} device profiles") print(f" innocuous_macs.db: {count} device profiles")
@@ -372,47 +385,85 @@ def create_ja3_fingerprints_db(db_path: str) -> None:
JA3 hashes uniquely identify TLS client implementations. JA3 hashes uniquely identify TLS client implementations.
Used by ja3_spoofer to match outbound traffic to common browsers. Used by ja3_spoofer to match outbound traffic to common browsers.
Table name and column names match what ja3_spoofer.py queries:
ja3_profiles(name, ja3_hash, description, cipher_suites,
extensions, elliptic_curves, ec_point_formats)
All list columns are stored as JSON arrays.
""" """
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA journal_mode=WAL")
conn.executescript(""" conn.executescript("""
CREATE TABLE IF NOT EXISTS fingerprints ( CREATE TABLE IF NOT EXISTS ja3_profiles (
ja3_hash TEXT PRIMARY KEY, name TEXT PRIMARY KEY,
client TEXT NOT NULL, ja3_hash TEXT NOT NULL,
version TEXT DEFAULT '' description TEXT DEFAULT '',
cipher_suites TEXT NOT NULL DEFAULT '[]',
extensions TEXT NOT NULL DEFAULT '[]',
elliptic_curves TEXT NOT NULL DEFAULT '[]',
ec_point_formats TEXT NOT NULL DEFAULT '[0]'
); );
CREATE INDEX IF NOT EXISTS idx_ja3_client ON fingerprints(client); CREATE INDEX IF NOT EXISTS idx_ja3_hash ON ja3_profiles(ja3_hash);
""") """)
# Common JA3 hashes -- these rotate with browser versions but # Mirror of BUILTIN_PROFILES in ja3_spoofer.py — ensures the DB has
# provide a baseline for spoofing. Updated at runtime via # the same data available for external queries and future updates.
# ja3er.com or similar feeds. import json as _json
ja3s = [ ja3s = [
("cd08e31494f9531f560d64c695473da9", "Chrome", "120+"), (
("b32309a26951912be7dba376398abc3b", "Chrome", "100-119"), "chrome_120_win",
("eb1d94daa7e0344597e756a1fb6e7054", "Firefox", "120+"), "cd08e31494f9531f560d64c695473da9",
("e4bb02b26cf670ba1d2e4942440a1cfc", "Firefox", "100-119"), "Chrome 120 on Windows 10/11",
("773906b0efdefa24a7f2b8eb6985bf37", "Safari", "17+"), _json.dumps([0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030,
("2ce0b4f7f6e119e26091e74e5c635cca", "Safari", "16"), 0xcca9, 0xcca8, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035]),
("56c70e24355b14b540300c0c15971b8a", "Edge", "120+"), _json.dumps([0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21]),
("555af378b96073da365e42db3052cf7a", "Edge", "100-119"), _json.dumps([0x001d, 0x0017, 0x0018]),
("3b5074b1b5d032e5620f69f9f700ff0e", "curl", "7.x"), _json.dumps([0]),
("456523fc94726331a4d5a2e1d40b2cd7", "Python requests", "2.x"), ),
("e7d705a3286e19ea42f587b344ee6865", "Wget", "1.x"), (
("6734f37431670b3ab4292b8f60f29984", "Java", "11+"), "firefox_121_win",
("e35c1eda3a26177f1a3f0aebdc2bd319", "Go", "1.19+"), "579ccef312d18482fc42e2b822ca2430",
"Firefox 121 on Windows 10/11",
_json.dumps([0x1301, 0x1303, 0x1302, 0xc02b, 0xc02f, 0xcca9, 0xcca8,
0xc02c, 0xc030, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035]),
_json.dumps([0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21]),
_json.dumps([0x001d, 0x0017, 0x0018, 0x0019]),
_json.dumps([0]),
),
(
"edge_120_win",
"b32309a26951912be7dba376398abc3b",
"Edge 120 on Windows 10/11",
_json.dumps([0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035]),
_json.dumps([0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21]),
_json.dumps([0x001d, 0x0017, 0x0018]),
_json.dumps([0]),
),
(
"chrome_120_linux",
"a17a3bfd385b62b1e15606dbd08c9f89",
"Chrome 120 on Linux",
_json.dumps([0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035]),
_json.dumps([0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21]),
_json.dumps([0x001d, 0x0017, 0x0018]),
_json.dumps([0]),
),
] ]
conn.executemany( conn.executemany(
"INSERT OR IGNORE INTO fingerprints (ja3_hash, client, version) VALUES (?, ?, ?)", "INSERT OR IGNORE INTO ja3_profiles "
ja3s "(name, ja3_hash, description, cipher_suites, extensions, elliptic_curves, ec_point_formats) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
ja3s,
) )
conn.commit() conn.commit()
count = conn.execute("SELECT COUNT(*) FROM fingerprints").fetchone()[0] count = conn.execute("SELECT COUNT(*) FROM ja3_profiles").fetchone()[0]
conn.close() conn.close()
print(f" ja3_fingerprints.db: {count} JA3 fingerprints") print(f" ja3_fingerprints.db: {count} JA3 profiles")
def main(): def main():