#!/usr/bin/python3 """ SetecSuite — Camera MITM Tool (PyQt6 GUI) Usage: sudo /usr/bin/python3 gui.py """ import os import sys import json import threading import signal sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QObject from PyQt6.QtGui import QFont, QTextCursor, QColor, QPalette, QAction from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout, QHBoxLayout, QPushButton, QPlainTextEdit, QLabel, QLineEdit, QTableWidget, QTableWidgetItem, QHeaderView, QFormLayout, QGroupBox, QComboBox, QStatusBar, QMessageBox, QSplitter, QTextEdit, QAbstractItemView, QCheckBox, ) from config import Config from utils.log import ( log, log_lines, init_logfile, close_logfile, lock, C_NONE, C_ERROR, C_SUCCESS, C_INFO, C_TRAFFIC, C_IMPORTANT, ) from services import arp_spoof, dns_spoof, http_server, udp_listener, sniffer, intruder_watch from utils import proto as proto_id from api import ubox_client, server as rest_server, fuzzer, firmware_fetch, cve_checks, ota_bucket_probe from api.fuzzer import KNOWN_ENDPOINTS from inject import packet from mitm import Controller # ─── Color map (Qt) ────────────────────────────────────────────── QT_COLORS = { C_NONE: QColor("#cccccc"), C_ERROR: QColor("#ff5555"), C_SUCCESS: QColor("#50fa7b"), C_INFO: QColor("#8be9fd"), C_TRAFFIC: QColor("#f1fa8c"), C_IMPORTANT: QColor("#ff79c6"), } # ─── Bridge: log_lines deque -> Qt signal ──────────────────────── class LogBridge(QObject): new_lines = pyqtSignal(list) # list of (line, color) class CloudBridge(QObject): response = pyqtSignal(str, object) # label, payload class CveBridge(QObject): result = pyqtSignal(str, object) # cve_id, result dict # ─── Main Window ───────────────────────────────────────────────── class MainWindow(QMainWindow): def __init__(self, ctrl): super().__init__() self.ctrl = ctrl self.setWindowTitle("SetecSuite — Camera MITM") self.resize(1400, 900) self._apply_dark_theme() self.bridge = LogBridge() self.bridge.new_lines.connect(self._append_log) self._last_log_idx = 0 self.cloud_bridge = CloudBridge() self.cloud_bridge.response.connect(self._cloud_set_response) self.cloud_response_signal = self.cloud_bridge.response self.cve_bridge = CveBridge() self.cve_bridge.result.connect(self._cve_set_status) self.cve_signal = self.cve_bridge.result self.tabs = QTabWidget() self.tabs.addTab(self._build_dashboard(), "Dashboard") self.tabs.addTab(self._build_log_tab(), "Live Log") self.tabs.addTab(self._build_intruder_tab(), "Intruders") self.tabs.addTab(self._build_cloud_tab(), "Cloud API") self.tabs.addTab(self._build_fuzzer_tab(), "Fuzzer") self.tabs.addTab(self._build_inject_tab(), "Inject") self.tabs.addTab(self._build_cve_tab(), "CVEs") self.tabs.addTab(self._build_config_tab(), "Config") self.tabs.addTab(self._build_help_tab(), "Help") self.setCentralWidget(self.tabs) self.status = QStatusBar() self.setStatusBar(self.status) # Periodic UI refresh self.refresh_timer = QTimer(self) self.refresh_timer.timeout.connect(self._tick) self.refresh_timer.start(300) # Start REST API threading.Thread( target=rest_server.start_server, args=(self.ctrl, self.ctrl.cfg["rest_port"]), daemon=True, ).start() log("SetecSuite GUI ready", C_SUCCESS) log(f"Target: {ctrl.cfg['camera_ip']} Us: {ctrl.cfg['our_ip']} Router: {ctrl.cfg['router_ip']}", C_INFO) # ── Theme ──────────────────────────────────────────────────── def _apply_dark_theme(self): self.setStyleSheet(""" QWidget { background: #1e1f29; color: #f8f8f2; font-family: 'JetBrains Mono','Fira Code',monospace; font-size: 11pt; } QTabWidget::pane { border: 1px solid #44475a; } QTabBar::tab { background: #282a36; padding: 8px 16px; border: 1px solid #44475a; } QTabBar::tab:selected { background: #44475a; color: #50fa7b; } QPushButton { background: #44475a; border: 1px solid #6272a4; padding: 6px 14px; border-radius: 3px; } QPushButton:hover { background: #6272a4; } QPushButton:pressed { background: #50fa7b; color: #282a36; } QPlainTextEdit, QTextEdit, QLineEdit { background: #282a36; border: 1px solid #44475a; selection-background-color: #44475a; } QHeaderView::section { background: #44475a; color: #f8f8f2; padding: 4px; border: none; } QTableWidget { background: #282a36; gridline-color: #44475a; } QTableWidget::item:selected { background: #44475a; } QGroupBox { border: 1px solid #44475a; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #8be9fd; } QStatusBar { background: #282a36; color: #50fa7b; } QLabel#bigState { font-size: 18pt; font-weight: bold; } QLabel.flag_on { color: #50fa7b; font-weight: bold; } QLabel.flag_off { color: #ff5555; } """) # ── Dashboard tab ──────────────────────────────────────────── def _build_dashboard(self): w = QWidget() layout = QVBoxLayout(w) ctrl_box = QGroupBox("MITM Control") cl = QHBoxLayout(ctrl_box) self.btn_start = QPushButton("▶ START MITM") self.btn_stop = QPushButton("⏹ STOP") self.btn_clear = QPushButton("Clear Log") self.btn_start.clicked.connect(lambda: threading.Thread(target=self.ctrl.start_services, daemon=True).start()) self.btn_stop.clicked.connect(lambda: threading.Thread(target=self.ctrl.stop_services, daemon=True).start()) self.btn_clear.clicked.connect(self._clear_log) cl.addWidget(self.btn_start) cl.addWidget(self.btn_stop) cl.addWidget(self.btn_clear) cl.addStretch() layout.addWidget(ctrl_box) self.state_label = QLabel("MITM: STOPPED") self.state_label.setObjectName("bigState") self.state_label.setStyleSheet("color: #ff5555; font-size: 18pt; font-weight: bold; padding: 10px;") layout.addWidget(self.state_label) flags_box = QGroupBox("Services (click to toggle)") self.flags_layout = QVBoxLayout(flags_box) self.svc_buttons = {} for name in ("arp", "dns", "http", "https", "udp10240", "udp20001", "sniffer", "intruder"): btn = QPushButton(f"● {name}: off") btn.setStyleSheet("text-align:left; color:#ff5555; padding:6px; background:#282a36;") btn.clicked.connect(lambda _, n=name: threading.Thread( target=self.ctrl.toggle_service, args=(n,), daemon=True).start()) self.svc_buttons[name] = btn self.flags_layout.addWidget(btn) layout.addWidget(flags_box) proto_box = QGroupBox("Protocols Seen") self.proto_layout = QVBoxLayout(proto_box) self.proto_label = QLabel("(none yet)") self.proto_label.setStyleSheet("color: #f1fa8c; font-family: monospace;") self.proto_layout.addWidget(self.proto_label) layout.addWidget(proto_box) info_box = QGroupBox("Target") il = QFormLayout(info_box) self.lbl_cam = QLabel(self.ctrl.cfg["camera_ip"]) self.lbl_us = QLabel(self.ctrl.cfg["our_ip"]) self.lbl_rtr = QLabel(self.ctrl.cfg["router_ip"]) self.lbl_mac = QLabel(self.ctrl.cfg["camera_mac"]) for lbl in (self.lbl_cam, self.lbl_us, self.lbl_rtr, self.lbl_mac): lbl.setStyleSheet("color: #f1fa8c; font-weight: bold;") il.addRow("Camera IP:", self.lbl_cam) il.addRow("Our IP:", self.lbl_us) il.addRow("Router IP:", self.lbl_rtr) il.addRow("Camera MAC:", self.lbl_mac) layout.addWidget(info_box) layout.addStretch() return w # ── Live log tab ───────────────────────────────────────────── def _build_log_tab(self): w = QWidget() layout = QVBoxLayout(w) bar = QHBoxLayout() bar.addWidget(QLabel("Filter:")) self.log_filter = QLineEdit() self.log_filter.setPlaceholderText("substring filter (live)…") bar.addWidget(self.log_filter) self.autoscroll_cb = QCheckBox("Autoscroll") self.autoscroll_cb.setChecked(True) bar.addWidget(self.autoscroll_cb) btn_clear = QPushButton("Clear") btn_clear.clicked.connect(self._clear_log) bar.addWidget(btn_clear) layout.addLayout(bar) self.log_view = QTextEdit() self.log_view.setReadOnly(True) self.log_view.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) f = QFont("JetBrains Mono", 10) f.setStyleHint(QFont.StyleHint.Monospace) self.log_view.setFont(f) layout.addWidget(self.log_view) return w # ── Intruder tab ───────────────────────────────────────────── def _build_intruder_tab(self): w = QWidget() layout = QVBoxLayout(w) head = QHBoxLayout() self.intruder_count = QLabel("0 events") self.intruder_count.setStyleSheet("color: #ff79c6; font-size: 14pt; font-weight: bold;") head.addWidget(self.intruder_count) head.addStretch() btn_clear = QPushButton("Clear") btn_clear.clicked.connect(lambda: (intruder_watch.clear_intruders(), self._refresh_intruders())) head.addWidget(btn_clear) layout.addLayout(head) self.intruder_table = QTableWidget(0, 5) self.intruder_table.setHorizontalHeaderLabels(["Time", "Kind", "Source", "Destination", "Detail"]) self.intruder_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.intruder_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.intruder_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) layout.addWidget(self.intruder_table) return w # ── Cloud API tab ──────────────────────────────────────────── def _build_cloud_tab(self): w = QWidget() layout = QVBoxLayout(w) cred_box = QGroupBox("UBox Credentials") cl = QFormLayout(cred_box) self.api_email = QLineEdit(self.ctrl.cfg.get("api_email", "")) self.api_pw = QLineEdit(self.ctrl.cfg.get("api_password", "")) self.api_pw.setEchoMode(QLineEdit.EchoMode.Password) cl.addRow("Email:", self.api_email) cl.addRow("Password:", self.api_pw) layout.addWidget(cred_box) self.cloud_status = QLabel("not logged in") self.cloud_status.setStyleSheet("color: #ff5555; font-weight: bold; padding: 4px;") layout.addWidget(self.cloud_status) btn_box = QHBoxLayout() actions = [ ("Login", self._cloud_login), ("Devices", lambda: self._cloud_call("devices", ubox_client.devices)), ("Firmware", lambda: self._cloud_call("firmware", ubox_client.check_firmware)), ("Services", lambda: self._cloud_call("services", ubox_client.device_services)), ("Families", lambda: self._cloud_call("families", ubox_client.families)), ] for label, fn in actions: b = QPushButton(label) b.clicked.connect(fn) btn_box.addWidget(b) layout.addLayout(btn_box) fw_box = QGroupBox("Firmware Download") fwl = QHBoxLayout(fw_box) fwl.addWidget(QLabel("host_version:")) self.fw_version = QLineEdit() self.fw_version.setPlaceholderText("blank = auto-try common versions") fwl.addWidget(self.fw_version) fw_btn = QPushButton("Download FW") fw_btn.clicked.connect(lambda: self._cloud_call( "download_fw", lambda cfg: firmware_fetch.check_and_download( cfg, host_version=(self.fw_version.text() or None) ), )) fwl.addWidget(fw_btn) probe_btn = QPushButton("Probe OTA Bucket") probe_btn.clicked.connect(lambda: self._cloud_call( "ota_probe", ota_bucket_probe.probe )) fwl.addWidget(probe_btn) layout.addWidget(fw_box) raw_box = QGroupBox("Raw POST") rl = QHBoxLayout(raw_box) self.raw_endpoint = QComboBox() self.raw_endpoint.setEditable(True) self.raw_endpoint.addItem("") # blank first for ep in sorted(set(KNOWN_ENDPOINTS)): self.raw_endpoint.addItem(ep) self.raw_endpoint.lineEdit().setPlaceholderText( f"pick one of {len(KNOWN_ENDPOINTS)} known endpoints, or type your own" ) self.raw_endpoint.setMinimumWidth(420) rl.addWidget(self.raw_endpoint, stretch=1) rb = QPushButton("Send") rb.clicked.connect(lambda: self._cloud_call( f"raw {self.raw_endpoint.currentText()}", lambda cfg: ubox_client.raw_request(cfg, self.raw_endpoint.currentText()) )) rl.addWidget(rb) oam_btn = QPushButton("OAM Send") oam_btn.setToolTip("Sign with OAM HMAC secret and post to oam.ubianet.com") oam_btn.clicked.connect(lambda: self._cloud_call( f"oam {self.raw_endpoint.currentText()}", lambda cfg: ubox_client.oam_post(self.raw_endpoint.currentText(), {}) )) rl.addWidget(oam_btn) oam_fuzz_btn = QPushButton("OAM Fuzz") oam_fuzz_btn.setToolTip("Probe ~50 candidate OAM admin endpoints") oam_fuzz_btn.clicked.connect(lambda: self._cloud_call( "oam_fuzz", lambda cfg: ubox_client.oam_fuzz(ubox_client.OAM_ENDPOINT_GUESSES) )) rl.addWidget(oam_fuzz_btn) layout.addWidget(raw_box) resp_box = QGroupBox("Response") rvl = QVBoxLayout(resp_box) self.cloud_response = QPlainTextEdit() self.cloud_response.setReadOnly(True) f = QFont("JetBrains Mono", 10) f.setStyleHint(QFont.StyleHint.Monospace) self.cloud_response.setFont(f) self.cloud_response.setPlaceholderText("API responses will appear here") rvl.addWidget(self.cloud_response) layout.addWidget(resp_box, stretch=1) return w def _cloud_set_response(self, label, obj): try: text = json.dumps(obj, indent=2, ensure_ascii=False) except Exception: text = repr(obj) self.cloud_response.setPlainText(f"=== {label} ===\n{text}") def _cloud_call(self, label, fn): def run(): try: result = fn(self.ctrl.cfg) if label == "devices" and isinstance(result, list): self.ctrl._devices = result self.cloud_response_signal.emit(label, result if result is not None else {"result": None}) except Exception as e: self.cloud_response_signal.emit(label, {"exception": str(e)}) threading.Thread(target=run, daemon=True).start() def _cloud_login(self): self.ctrl.cfg["api_email"] = self.api_email.text() self.ctrl.cfg["api_password"] = self.api_pw.text() self.ctrl.cfg.save() def run(): ok = ubox_client.login(self.ctrl.cfg) payload = { "logged_in": bool(ok), "token": self.ctrl.cfg.get("api_token", ""), "email": self.ctrl.cfg.get("api_email", ""), } self.cloud_response_signal.emit("login", payload) threading.Thread(target=run, daemon=True).start() # ── Fuzzer tab ─────────────────────────────────────────────── def _build_fuzzer_tab(self): w = QWidget() layout = QVBoxLayout(w) bb = QHBoxLayout() b1 = QPushButton("Fuzz Endpoints") b1.clicked.connect(lambda: threading.Thread(target=self.ctrl.run_fuzz_endpoints, daemon=True).start()) bb.addWidget(b1) b2 = QPushButton("Fuzz Auth") b2.clicked.connect(lambda: threading.Thread(target=self.ctrl.run_fuzz_auth, daemon=True).start()) bb.addWidget(b2) bb.addStretch() layout.addLayout(bb) param_box = QGroupBox("Param Fuzz") pl = QHBoxLayout(param_box) self.fuzz_ep = QLineEdit() self.fuzz_ep.setPlaceholderText("endpoint name") pl.addWidget(self.fuzz_ep) b3 = QPushButton("Run") b3.clicked.connect(lambda: threading.Thread( target=self.ctrl.run_fuzz_params, args=(self.fuzz_ep.text(),), daemon=True ).start()) pl.addWidget(b3) layout.addWidget(param_box) self.fuzz_status = QLabel("Idle") layout.addWidget(self.fuzz_status) b4 = QPushButton("Stop fuzzer") b4.clicked.connect(lambda: (self.ctrl.fuzzer and self.ctrl.fuzzer.stop())) layout.addWidget(b4) layout.addStretch() return w # ── Inject tab ─────────────────────────────────────────────── def _build_inject_tab(self): w = QWidget() layout = QVBoxLayout(w) udp_box = QGroupBox("UDP Inject") ul = QFormLayout(udp_box) self.udp_ip = QLineEdit(self.ctrl.cfg["camera_ip"]) self.udp_port = QLineEdit("10240") self.udp_payload = QLineEdit() self.udp_payload.setPlaceholderText("hex payload, e.g. deadbeef") ul.addRow("Dst IP:", self.udp_ip) ul.addRow("Dst port:", self.udp_port) ul.addRow("Payload:", self.udp_payload) b = QPushButton("Send UDP") b.clicked.connect(self._send_udp) ul.addRow("", b) layout.addWidget(udp_box) arp_box = QGroupBox("ARP Reply") al = QFormLayout(arp_box) self.arp_src = QLineEdit(self.ctrl.cfg["router_ip"]) self.arp_dst = QLineEdit(self.ctrl.cfg["camera_ip"]) al.addRow("Src IP (spoof):", self.arp_src) al.addRow("Dst IP:", self.arp_dst) b2 = QPushButton("Send ARP") b2.clicked.connect(lambda: packet.inject(self.ctrl.cfg, { "type": "arp_reply", "src_ip": self.arp_src.text(), "dst_ip": self.arp_dst.text() })) al.addRow("", b2) layout.addWidget(arp_box) layout.addStretch() return w def _send_udp(self): try: packet.inject(self.ctrl.cfg, { "type": "udp", "dst_ip": self.udp_ip.text(), "dst_port": int(self.udp_port.text()), "payload": self.udp_payload.text(), "payload_hex": True, }) except Exception as e: QMessageBox.warning(self, "Inject error", str(e)) # ── Config tab ─────────────────────────────────────────────── def _build_config_tab(self): w = QWidget() layout = QFormLayout(w) self.cfg_inputs = {} for k, v in self.ctrl.cfg.items(): if "password" in k or "token" in k: continue le = QLineEdit(str(v)) self.cfg_inputs[k] = le layout.addRow(k, le) b = QPushButton("Save Config") b.clicked.connect(self._save_config) layout.addRow("", b) return w # ── CVE tab ────────────────────────────────────────────────── def _build_cve_tab(self): w = QWidget() layout = QVBoxLayout(w) head = QHBoxLayout() title = QLabel("CVE verification — original probes, non-destructive") title.setStyleSheet("color:#ff79c6; font-size:13pt; font-weight:bold;") head.addWidget(title) head.addStretch() b_run = QPushButton("▶ Run All Verifications") b_run.clicked.connect(self._cve_run_all) head.addWidget(b_run) b_rep = QPushButton("📝 Generate Report") b_rep.clicked.connect(self._cve_generate_report) head.addWidget(b_rep) layout.addLayout(head) # Card per CVE self.cve_cards = {} cve_specs = [ ("CVE-2025-12636", "Ubia Ubox cloud API leaks IOTC device credentials in plaintext", "The cloud `user/device_list` endpoint returns `cam_user` / `cam_pwd` " "in plaintext to any authenticated owner. These are the IOTC P2P " "device-auth credentials and grant full local control. Verifier calls " "the endpoint and inspects the response.", cve_checks.verify_cve_2025_12636), ("CVE-2021-28372", "ThroughTek Kalay P2P UID-based session hijack (UBIC rebrand)", "Kalay master server identifies devices by UID alone. An attacker " "knowing the UID can register the same UID against the master and " "intercept the next legitimate client login. We verify preconditions " "(UID format + camera P2P stack alive) without performing the spoof " "registration.", cve_checks.verify_cve_2021_28372), ("CVE-2023-6322 / 6323 / 6324", "ThroughTek Kalay LAN-side memory corruption + auth bypass chain", "Three flaws in the Kalay LAN protocol parser: an auth bypass, a " "heap overflow, and a stack overflow. Verifier sends only safe small " "probes — no overflow payloads — and reports based on stack " "fingerprint and pre/post liveness.", cve_checks.verify_cve_2023_6322_chain), ] for cve_id, title_txt, desc, verifier in cve_specs: card = QGroupBox(cve_id) cl = QVBoxLayout(card) t = QLabel(f"{title_txt}") t.setWordWrap(True) t.setStyleSheet("color:#8be9fd;") cl.addWidget(t) d = QLabel(desc) d.setWordWrap(True) d.setStyleSheet("color:#cccccc; padding:4px;") cl.addWidget(d) row = QHBoxLayout() status = QLabel("● not run") status.setStyleSheet("color:#888888; font-weight:bold;") row.addWidget(status) row.addStretch() btn = QPushButton("Verify") btn.clicked.connect(lambda _, v=verifier, cid=cve_id: self._cve_run_one(cid, v)) row.addWidget(btn) cl.addLayout(row) evidence = QPlainTextEdit() evidence.setReadOnly(True) evidence.setMaximumHeight(120) evidence.setPlaceholderText("evidence + raw artifacts will appear here") cl.addWidget(evidence) self.cve_cards[cve_id] = { "status": status, "evidence": evidence, "result": None, } layout.addWidget(card) layout.addStretch() return w def _cve_set_status(self, cve_id, result): card = self.cve_cards.get(cve_id) if not card: return card["result"] = result s = result.get("status", "?") color = {"VULN": "#ff5555", "NOT_VULN": "#50fa7b", "UNKNOWN": "#f1fa8c", "ERROR": "#ff79c6"}.get(s, "#cccccc") card["status"].setText(f"● {s}") card["status"].setStyleSheet(f"color:{color}; font-weight:bold;") text = result.get("evidence", "") + "\n\n" + json.dumps(result.get("details", {}), indent=2, ensure_ascii=False) card["evidence"].setPlainText(text) def _cve_run_one(self, cve_id, verifier): def run(): try: r = verifier(self.ctrl.cfg) except Exception as e: r = {"cve": cve_id, "status": "ERROR", "title": "exception", "evidence": str(e), "details": {}} self.cve_signal.emit(cve_id, r) threading.Thread(target=run, daemon=True).start() def _cve_run_all(self): def run(): for cve_id, fn in [ ("CVE-2025-12636", cve_checks.verify_cve_2025_12636), ("CVE-2021-28372", cve_checks.verify_cve_2021_28372), ("CVE-2023-6322 / 6323 / 6324", cve_checks.verify_cve_2023_6322_chain), ]: try: r = fn(self.ctrl.cfg) except Exception as e: r = {"cve": cve_id, "status": "ERROR", "title": "exception", "evidence": str(e), "details": {}} self.cve_signal.emit(cve_id, r) threading.Thread(target=run, daemon=True).start() def _cve_generate_report(self): results = [] for cve_id, card in self.cve_cards.items(): if card["result"]: results.append(card["result"]) if not results: QMessageBox.information(self, "Report", "Run at least one verification first.") return try: path = cve_checks.build_report(self.ctrl.cfg, results) QMessageBox.information(self, "Report written", f"Saved to:\n{path}") except Exception as e: QMessageBox.warning(self, "Report failed", str(e)) # ── Help tab ───────────────────────────────────────────────── def _build_help_tab(self): w = QWidget() layout = QVBoxLayout(w) view = QTextEdit() view.setReadOnly(True) view.setHtml("""
Target: Javiscam/UBox cam (TUTK Kalay) at the IP shown in the Dashboard.
Always running on http://127.0.0.1:9090. Endpoints: /status, /logs, /devices, /config, /fuzz/results, /start, /stop, /command, /api, /fuzz/endpoints, /fuzz/params, /fuzz/auth, /inject
~/setec_suite/cam-mitm/config.json/root/dumps/mitm_logs/mitm.log (rotates at 1 GiB)/root/dumps/mitm_logs/raw_*.binsudo /home/snake/setec_suite/cam-mitm/regen_cert.shsudo /usr/bin/python3 /home/snake/setec_suite/cam-mitm/gui.py
(Custom Python 3.14 build at /usr/local/bin lacks _curses and PyQt6 — must use the system /usr/bin/python3)
""") layout.addWidget(view) return w def _save_config(self): for k, le in self.cfg_inputs.items(): old = self.ctrl.cfg[k] v = le.text() try: if isinstance(old, int): v = int(v) elif isinstance(old, float): v = float(v) except ValueError: pass self.ctrl.cfg[k] = v self.ctrl.cfg.save() log("Config saved from GUI", C_SUCCESS) # ── Periodic refresh ───────────────────────────────────────── def _tick(self): # New log lines with lock: total = len(log_lines) if total < self._last_log_idx: self._last_log_idx = 0 # deque rolled new = list(log_lines)[self._last_log_idx:] self._last_log_idx = total if new: self.bridge.new_lines.emit(new) # State if self.ctrl.services_running: self.state_label.setText("MITM: RUNNING") self.state_label.setStyleSheet("color: #50fa7b; font-size: 18pt; font-weight: bold; padding: 10px;") else: self.state_label.setText("MITM: STOPPED") self.state_label.setStyleSheet("color: #ff5555; font-size: 18pt; font-weight: bold; padding: 10px;") for name, btn in self.svc_buttons.items(): on = self.ctrl.flags.get(name, False) btn.setText(f"● {name}: {'ON' if on else 'off'}") color = "#50fa7b" if on else "#ff5555" btn.setStyleSheet(f"text-align:left; color:{color}; padding:6px; background:#282a36;") # Protocols seen counts = proto_id.seen_counts() if counts: txt = " ".join(f"{k}={v}" for k, v in sorted(counts.items(), key=lambda x: -x[1])) self.proto_label.setText(txt) # Intruders self._refresh_intruders() # Cloud status if self.ctrl.cfg.get("api_token"): tok = self.ctrl.cfg["api_token"][:16] self.cloud_status.setText(f"logged in as {self.ctrl.cfg.get('api_email','?')} token={tok}…") self.cloud_status.setStyleSheet("color: #50fa7b; font-weight: bold; padding: 4px;") else: self.cloud_status.setText("not logged in") self.cloud_status.setStyleSheet("color: #ff5555; font-weight: bold; padding: 4px;") # Status bar n_devs = len(self.ctrl._devices) if self.ctrl._devices else 0 self.status.showMessage( f"REST :{self.ctrl.cfg['rest_port']} | Devices: {n_devs} | " f"Token: {'yes' if self.ctrl.cfg['api_token'] else 'no'} | " f"Intruders: {len(intruder_watch.get_intruders())}" ) def _refresh_intruders(self): items = intruder_watch.get_intruders() self.intruder_count.setText(f"{len(items)} events") if self.intruder_table.rowCount() != len(items): self.intruder_table.setRowCount(len(items)) for i, e in enumerate(items): self.intruder_table.setItem(i, 0, QTableWidgetItem(e["ts"])) kind_item = QTableWidgetItem(e["kind"]) kind_item.setForeground(QColor("#ff79c6")) self.intruder_table.setItem(i, 1, kind_item) self.intruder_table.setItem(i, 2, QTableWidgetItem(e["src"])) self.intruder_table.setItem(i, 3, QTableWidgetItem(e["dst"])) self.intruder_table.setItem(i, 4, QTableWidgetItem(e["detail"])) # ── Log append ─────────────────────────────────────────────── def _append_log(self, lines): flt = self.log_filter.text().lower() autoscroll = self.autoscroll_cb.isChecked() # Preserve current scroll position when autoscroll off sb = self.log_view.verticalScrollBar() old_pos = sb.value() old_user_cursor = self.log_view.textCursor() write_cursor = QTextCursor(self.log_view.document()) write_cursor.movePosition(QTextCursor.MoveOperation.End) for line, color in lines: if flt and flt not in line.lower(): continue fmt = write_cursor.charFormat() fmt.setForeground(QT_COLORS.get(color, QT_COLORS[C_NONE])) write_cursor.setCharFormat(fmt) write_cursor.insertText(line + "\n") if autoscroll: self.log_view.moveCursor(QTextCursor.MoveOperation.End) self.log_view.ensureCursorVisible() else: self.log_view.setTextCursor(old_user_cursor) sb.setValue(old_pos) def _clear_log(self): self.log_view.clear() with lock: log_lines.clear() self._last_log_idx = 0 def closeEvent(self, ev): if self.ctrl.services_running: self.ctrl.stop_services() self.ctrl.running = False close_logfile() ev.accept() def main(): if os.geteuid() != 0: print("Run with: sudo /usr/bin/python3 gui.py") sys.exit(1) ctrl = Controller() os.makedirs(ctrl.cfg["log_dir"], exist_ok=True) init_logfile(f"{ctrl.cfg['log_dir']}/mitm.log") signal.signal(signal.SIGINT, signal.SIG_DFL) app = QApplication(sys.argv) win = MainWindow(ctrl) win.show() rc = app.exec() if ctrl.services_running: ctrl.stop_services() close_logfile() sys.exit(rc) if __name__ == "__main__": main()