mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 18:36:55 +00:00
feat(eye-remote): Add multi-mode display system v1.9.0
Eye Remote Interactive UI enhancements: - TTY mode: Serial terminal display from /dev/ttyGS0 - Flash mode: Progress bar with speed/ETA for USB transfers - Auth mode: QR code generation for backup authentication - Mode detection via /etc/secubox/gadget-mode Hub service VM compatibility fix: - Changed from Unix socket to TCP port 8001 - Updated nginx configs for TCP proxy - Fixes 502 errors in VirtualBox VMs Also includes: - FAQ/Troubleshooting wiki page with GitHub issue links - Kiosk launcher --no-sandbox fix for VMs - Profile Generator GUI mockup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
33304e7b6f
commit
c4b7ac0f7f
|
|
@ -3,6 +3,36 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-04-29
|
||||
|
||||
### Session 73 — Eye Remote Interactive v1.9.0
|
||||
|
||||
**Feature:** Multi-mode USB gadget display system for Eye Remote
|
||||
|
||||
**Files Modified:**
|
||||
- `remote-ui/round/fb_dashboard.py` — Added mode detection, TTY terminal, flash progress, auth QR
|
||||
- `packages/secubox-hub/debian/secubox-hub.service` — Changed to TCP binding (port 8001)
|
||||
- `packages/secubox-hub/nginx/hub.conf` — Changed to TCP proxy
|
||||
- `common/nginx/modules.d/hub.conf` — Changed to TCP proxy
|
||||
|
||||
**New Classes:**
|
||||
- `SerialTerminal` — Read serial console output for TTY mode
|
||||
- `FlashProgress` — Track USB mass storage transfer progress
|
||||
- `AuthState` — QR code generation for backup authentication
|
||||
|
||||
**New Functions:**
|
||||
- `get_gadget_mode()` — Read current USB gadget mode from /etc/secubox/gadget-mode
|
||||
- `draw_terminal()` — Render serial terminal output on round display
|
||||
- `draw_flash_progress()` — Render flash transfer progress bar
|
||||
- `draw_auth_mode()` — Render QR code authentication screen
|
||||
|
||||
**Fixes:**
|
||||
- Hub service changed from Unix socket to TCP (VM compatibility)
|
||||
- FAQ and wiki updated with troubleshooting for common issues
|
||||
- Kiosk launcher fixed for VM sandbox issues (--no-sandbox flag)
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-28
|
||||
|
||||
### Session 72 — v2.1.1 Release: Build and API Fixes
|
||||
|
|
|
|||
|
|
@ -1,5 +1,49 @@
|
|||
# WIP — Work In Progress
|
||||
*Mis à jour : 2026-04-28 (Session 71)*
|
||||
*Mis à jour : 2026-04-29 (Session 73)*
|
||||
|
||||
---
|
||||
|
||||
## ✅ Complété (Session 73) — Eye Remote Interactive v1.9.0
|
||||
|
||||
### Mode-Aware Display System ✅
|
||||
|
||||
**Feature:** Multi-mode USB gadget display system for Eye Remote
|
||||
|
||||
**Files Modified:**
|
||||
- `remote-ui/round/fb_dashboard.py` — Added mode detection, TTY terminal, flash progress, auth QR
|
||||
- `packages/secubox-hub/debian/secubox-hub.service` — Changed to TCP binding (port 8001)
|
||||
- `packages/secubox-hub/nginx/hub.conf` — Changed to TCP proxy
|
||||
- `common/nginx/modules.d/hub.conf` — Changed to TCP proxy
|
||||
|
||||
**New Features:**
|
||||
| Mode | Display | Function |
|
||||
|------|---------|----------|
|
||||
| TTY | Serial terminal | Real-time U-Boot/console output from /dev/ttyGS0 |
|
||||
| FLASH | Progress bar | Transfer progress with speed/ETA for eMMC flashing |
|
||||
| AUTH | QR code | Backup authentication code for FIDO2 security |
|
||||
| NORMAL | Dashboard | Standard metrics display |
|
||||
| DEBUG | Dashboard | Network + storage + serial combined |
|
||||
|
||||
**TTY Mode Features:**
|
||||
- SerialTerminal class reading from /dev/ttyGS0 at 115200 baud
|
||||
- Real-time line buffering with ASCII filtering
|
||||
- Monospace font rendering adapted to round display
|
||||
- Automatic mode switching via /etc/secubox/gadget-mode
|
||||
|
||||
**Flash Mode Features:**
|
||||
- FlashProgress class tracking /var/lib/secubox-flash.img transfers
|
||||
- Progress bar with percentage, speed (MB/s), and ETA
|
||||
- Status: WAITING/TRANSFERRING/COMPLETE indicators
|
||||
|
||||
**Auth Mode Features:**
|
||||
- AuthState class with QR code generation
|
||||
- Device ID based on /etc/machine-id
|
||||
- Backup authentication URL format: secubox-auth://device_id/challenge
|
||||
- State machine: idle → pending → approved/denied
|
||||
|
||||
**Hub Service Fix:**
|
||||
- Changed from Unix socket to TCP port 8001 for VM compatibility
|
||||
- Issue documented in FAQ-Troubleshooting.md and GitHub #34
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# /etc/nginx/secubox.d/hub.conf
|
||||
# Installed by secubox-hub package
|
||||
# Using TCP port for VM compatibility (Unix socket has issues in some VMs)
|
||||
location /api/v1/hub/ {
|
||||
proxy_pass http://unix:/run/secubox/hub.sock:/;
|
||||
proxy_pass http://127.0.0.1:8001/;
|
||||
include /etc/nginx/snippets/secubox-proxy.conf;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -432,8 +432,8 @@ create_session() {
|
|||
log "Real hardware detected - GPU acceleration enabled"
|
||||
else
|
||||
# VM - disable GPU to prevent crashes
|
||||
chromium_gpu_flags="--disable-gpu --disable-gpu-compositing --disable-software-rasterizer"
|
||||
log "VM detected ($vm_type) - GPU disabled"
|
||||
chromium_gpu_flags="--disable-gpu --disable-gpu-compositing --disable-software-rasterizer --no-sandbox"
|
||||
log "VM detected ($vm_type) - GPU disabled, sandbox disabled for VM"
|
||||
fi
|
||||
|
||||
# Write GPU flags for session to read
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ WorkingDirectory=/usr/lib/secubox/hub
|
|||
ExecStartPre=+/bin/mkdir -p /run/secubox
|
||||
ExecStartPre=+/bin/chown secubox:secubox /run/secubox
|
||||
ExecStartPre=+/bin/chmod 775 /run/secubox
|
||||
# TCP binding for VM compatibility (Unix socket has issues in some VMs)
|
||||
ExecStart=/usr/bin/python3 -m uvicorn api.main:app \
|
||||
--uds /run/secubox/hub.sock \
|
||||
--host 127.0.0.1 --port 8001 \
|
||||
--log-level warning
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# /etc/nginx/secubox.d/hub.conf
|
||||
# Installed by secubox-hub package
|
||||
# Using TCP port for VM compatibility (Unix socket has issues in some VMs)
|
||||
location /api/v1/hub/ {
|
||||
proxy_pass http://unix:/run/secubox/hub.sock:/;
|
||||
proxy_pass http://127.0.0.1:8001/;
|
||||
include /etc/nginx/snippets/secubox-proxy.conf;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,25 @@ MODULES = {
|
|||
# Eye Agent Unix socket
|
||||
AGENT_SOCKET = '/run/secubox-eye/metrics.sock'
|
||||
|
||||
# Gadget mode configuration
|
||||
MODE_FILE = '/etc/secubox/gadget-mode'
|
||||
SERIAL_DEV = '/dev/ttyGS0'
|
||||
SERIAL_BAUD = 115200
|
||||
|
||||
# Terminal display settings
|
||||
TERM_FONT_SIZE = 12
|
||||
TERM_LINES = 28 # Lines visible in round display
|
||||
TERM_COLS = 45 # Chars per line (fits 480px with 12px mono font)
|
||||
|
||||
# Flash mode settings
|
||||
FLASH_IMAGE_FILE = '/var/lib/secubox-flash.img'
|
||||
FLASH_STATS_FILE = '/sys/kernel/config/usb_gadget/secubox/functions/mass_storage.0/lun.0/file'
|
||||
FLASH_PROGRESS_FILE = '/run/secubox-flash-progress'
|
||||
|
||||
# Auth mode settings
|
||||
AUTH_STATE_FILE = '/run/secubox-auth-state.json'
|
||||
AUTH_QR_SIZE = 200 # QR code size in pixels
|
||||
|
||||
# Module icons directory (relative to script location)
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ICONS_DIR = os.path.join(SCRIPT_DIR, 'assets', 'icons')
|
||||
|
|
@ -345,6 +364,629 @@ def format_uptime(seconds):
|
|||
return f'up {hours}h{minutes:02d}'
|
||||
|
||||
|
||||
def get_gadget_mode() -> str:
|
||||
"""Read current USB gadget mode.
|
||||
|
||||
Returns:
|
||||
Mode string: 'normal', 'flash', 'debug', 'tty', 'auth', or 'normal' as default
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(MODE_FILE):
|
||||
with open(MODE_FILE, 'r') as f:
|
||||
mode = f.read().strip().lower()
|
||||
if mode in ('normal', 'flash', 'debug', 'tty', 'auth'):
|
||||
return mode
|
||||
except:
|
||||
pass
|
||||
return 'normal'
|
||||
|
||||
|
||||
class SerialTerminal:
|
||||
"""Read and buffer serial console output for TTY mode display."""
|
||||
|
||||
def __init__(self, device=SERIAL_DEV, baud=SERIAL_BAUD, max_lines=100):
|
||||
self.device = device
|
||||
self.baud = baud
|
||||
self.max_lines = max_lines
|
||||
self.lines: list[str] = []
|
||||
self.serial = None
|
||||
self._buffer = ''
|
||||
|
||||
def open(self) -> bool:
|
||||
"""Open serial port for reading."""
|
||||
try:
|
||||
import serial
|
||||
self.serial = serial.Serial(
|
||||
self.device,
|
||||
self.baud,
|
||||
timeout=0.1,
|
||||
rtscts=False,
|
||||
dsrdtr=False
|
||||
)
|
||||
return True
|
||||
except ImportError:
|
||||
print('pyserial not installed')
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f'Serial open error: {e}')
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""Close serial port."""
|
||||
if self.serial:
|
||||
try:
|
||||
self.serial.close()
|
||||
except:
|
||||
pass
|
||||
self.serial = None
|
||||
|
||||
def read(self) -> bool:
|
||||
"""Read available data from serial port.
|
||||
|
||||
Returns:
|
||||
True if new data was read
|
||||
"""
|
||||
if not self.serial:
|
||||
return False
|
||||
|
||||
try:
|
||||
if self.serial.in_waiting > 0:
|
||||
data = self.serial.read(self.serial.in_waiting)
|
||||
text = data.decode('utf-8', errors='replace')
|
||||
self._buffer += text
|
||||
|
||||
# Process buffer into lines
|
||||
while '\n' in self._buffer:
|
||||
line, self._buffer = self._buffer.split('\n', 1)
|
||||
# Strip CR and clean control chars (keep basic ASCII)
|
||||
line = line.rstrip('\r')
|
||||
line = ''.join(c if 32 <= ord(c) < 127 else ' ' for c in line)
|
||||
self.lines.append(line)
|
||||
|
||||
# Trim old lines
|
||||
if len(self.lines) > self.max_lines:
|
||||
self.lines = self.lines[-self.max_lines:]
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'Serial read error: {e}')
|
||||
|
||||
return False
|
||||
|
||||
def get_display_lines(self, count=TERM_LINES) -> list[str]:
|
||||
"""Get last N lines for display.
|
||||
|
||||
Args:
|
||||
count: Number of lines to return
|
||||
|
||||
Returns:
|
||||
List of strings, each truncated to TERM_COLS
|
||||
"""
|
||||
display = self.lines[-count:] if self.lines else []
|
||||
# Pad to fill display
|
||||
while len(display) < count:
|
||||
display.insert(0, '')
|
||||
# Truncate long lines
|
||||
return [line[:TERM_COLS] for line in display]
|
||||
|
||||
def add_simulated_line(self, text: str):
|
||||
"""Add a line to the buffer (for testing without serial)."""
|
||||
self.lines.append(text[:TERM_COLS])
|
||||
if len(self.lines) > self.max_lines:
|
||||
self.lines = self.lines[-self.max_lines:]
|
||||
|
||||
|
||||
def draw_terminal(term: SerialTerminal, mode: str = 'tty') -> Image.Image:
|
||||
"""Draw a terminal display for TTY/serial mode.
|
||||
|
||||
Args:
|
||||
term: SerialTerminal instance with buffered output
|
||||
mode: Current gadget mode (tty, debug, etc.)
|
||||
|
||||
Returns:
|
||||
PIL Image ready for framebuffer
|
||||
"""
|
||||
img = Image.new('RGBA', (WIDTH, HEIGHT), BG_COLOR + (255,))
|
||||
draw = ImageDraw.Draw(img)
|
||||
cx, cy = WIDTH // 2, HEIGHT // 2
|
||||
|
||||
# Load monospace font
|
||||
try:
|
||||
font_mono = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', TERM_FONT_SIZE)
|
||||
font_small = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 10)
|
||||
except:
|
||||
font_mono = ImageFont.load_default()
|
||||
font_small = font_mono
|
||||
|
||||
# Draw circular mask/border
|
||||
draw.ellipse([5, 5, WIDTH-5, HEIGHT-5], outline=(40, 40, 50), width=2)
|
||||
|
||||
# Header bar
|
||||
header = f'TTY MODE — {mode.upper()}'
|
||||
bbox = draw.textbbox((0, 0), header, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.rectangle([cx-tw//2-10, 8, cx+tw//2+10, 24], fill=(30, 80, 50))
|
||||
draw.text((cx - tw//2, 10), header, fill=(0, 255, 100), font=font_small)
|
||||
|
||||
# Terminal area - calculate visible region (centered circle)
|
||||
# For round display, we need to shrink text area toward center
|
||||
term_top = 30
|
||||
term_left = 25
|
||||
term_right = WIDTH - 25
|
||||
line_height = TERM_FONT_SIZE + 2
|
||||
|
||||
# Get display lines
|
||||
lines = term.get_display_lines(TERM_LINES)
|
||||
|
||||
# Draw lines with subtle alternating background
|
||||
y = term_top
|
||||
for i, line in enumerate(lines):
|
||||
# Skip lines outside visible circle area
|
||||
# Simple approximation: reduce width near top/bottom
|
||||
dist_from_center = abs(y + line_height//2 - cy)
|
||||
if dist_from_center > 220: # Outside circle
|
||||
y += line_height
|
||||
continue
|
||||
|
||||
# Calculate available width at this y position
|
||||
if dist_from_center < 200:
|
||||
# Use Pythagorean theorem for circle
|
||||
half_width = int(math.sqrt(220**2 - dist_from_center**2))
|
||||
else:
|
||||
half_width = 50
|
||||
|
||||
line_x = max(term_left, cx - half_width)
|
||||
max_chars = (half_width * 2) // (TERM_FONT_SIZE // 2) # Approximate chars
|
||||
display_line = line[:max_chars]
|
||||
|
||||
# Alternate background for readability
|
||||
if i % 2 == 0 and display_line.strip():
|
||||
draw.rectangle([line_x, y, cx + half_width, y + line_height], fill=(15, 15, 20))
|
||||
|
||||
# Draw text
|
||||
if display_line.strip():
|
||||
draw.text((line_x, y), display_line, fill=(0, 255, 100), font=font_mono)
|
||||
|
||||
y += line_height
|
||||
|
||||
# Footer with status
|
||||
footer = f'● SERIAL @ {SERIAL_BAUD}'
|
||||
bbox = draw.textbbox((0, 0), footer, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, HEIGHT - 22), footer, fill=(100, 100, 120), font=font_small)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
class FlashProgress:
|
||||
"""Track flash/mass storage transfer progress."""
|
||||
|
||||
def __init__(self, image_file=FLASH_IMAGE_FILE):
|
||||
self.image_file = image_file
|
||||
self.total_size = 0
|
||||
self.bytes_read = 0
|
||||
self.start_time = time.time()
|
||||
self._last_bytes = 0
|
||||
self._last_time = time.time()
|
||||
self.speed_mbps = 0.0
|
||||
self.active = False
|
||||
|
||||
# Get image size
|
||||
try:
|
||||
if os.path.exists(image_file):
|
||||
self.total_size = os.path.getsize(image_file)
|
||||
print(f'Flash image: {image_file} ({self.total_size / (1024*1024):.1f} MB)')
|
||||
except:
|
||||
pass
|
||||
|
||||
def update(self) -> bool:
|
||||
"""Update progress by reading from /proc/diskstats or similar.
|
||||
|
||||
Returns:
|
||||
True if progress changed
|
||||
"""
|
||||
# Try to read progress from a status file written by gadget driver
|
||||
try:
|
||||
if os.path.exists(FLASH_PROGRESS_FILE):
|
||||
with open(FLASH_PROGRESS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
self.bytes_read = data.get('bytes_read', 0)
|
||||
self.active = data.get('active', False)
|
||||
|
||||
now = time.time()
|
||||
if now > self._last_time:
|
||||
elapsed = now - self._last_time
|
||||
bytes_diff = self.bytes_read - self._last_bytes
|
||||
if elapsed > 0 and bytes_diff > 0:
|
||||
self.speed_mbps = (bytes_diff / elapsed) / (1024 * 1024)
|
||||
self._last_bytes = self.bytes_read
|
||||
self._last_time = now
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fallback: check if backing file is being read via /proc
|
||||
try:
|
||||
# Use stat to detect access time changes
|
||||
stat = os.stat(self.image_file)
|
||||
# This is a rough heuristic - actual bytes read would need eBPF/tracing
|
||||
self.active = (time.time() - stat.st_atime) < 5
|
||||
except:
|
||||
self.active = False
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def percent(self) -> float:
|
||||
"""Get completion percentage."""
|
||||
if self.total_size <= 0:
|
||||
return 0.0
|
||||
return min(100.0, (self.bytes_read / self.total_size) * 100)
|
||||
|
||||
@property
|
||||
def eta_seconds(self) -> int:
|
||||
"""Estimate time remaining."""
|
||||
if self.speed_mbps <= 0 or self.total_size <= 0:
|
||||
return -1
|
||||
remaining_bytes = self.total_size - self.bytes_read
|
||||
return int(remaining_bytes / (self.speed_mbps * 1024 * 1024))
|
||||
|
||||
def format_size(self, bytes_val: int) -> str:
|
||||
"""Format bytes as human-readable string."""
|
||||
if bytes_val >= 1024 * 1024 * 1024:
|
||||
return f'{bytes_val / (1024*1024*1024):.1f} GB'
|
||||
elif bytes_val >= 1024 * 1024:
|
||||
return f'{bytes_val / (1024*1024):.1f} MB'
|
||||
elif bytes_val >= 1024:
|
||||
return f'{bytes_val / 1024:.1f} KB'
|
||||
return f'{bytes_val} B'
|
||||
|
||||
|
||||
def draw_flash_progress(progress: FlashProgress) -> Image.Image:
|
||||
"""Draw flash mode progress screen.
|
||||
|
||||
Args:
|
||||
progress: FlashProgress instance
|
||||
|
||||
Returns:
|
||||
PIL Image ready for framebuffer
|
||||
"""
|
||||
img = Image.new('RGBA', (WIDTH, HEIGHT), BG_COLOR + (255,))
|
||||
draw = ImageDraw.Draw(img)
|
||||
cx, cy = WIDTH // 2, HEIGHT // 2
|
||||
|
||||
# Load fonts
|
||||
try:
|
||||
font_large = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 36)
|
||||
font_medium = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 16)
|
||||
font_small = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 12)
|
||||
except:
|
||||
font_large = ImageFont.load_default()
|
||||
font_medium = font_large
|
||||
font_small = font_large
|
||||
|
||||
# Draw circular border
|
||||
draw.ellipse([10, 10, WIDTH-10, HEIGHT-10], outline=(40, 40, 50), width=2)
|
||||
|
||||
# Header
|
||||
header = 'FLASH MODE'
|
||||
bbox = draw.textbbox((0, 0), header, font=font_medium)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, 25), header, fill=(255, 100, 0), font=font_medium)
|
||||
|
||||
# Flash icon (simplified USB icon)
|
||||
icon = load_module_icon('BOOT', 96)
|
||||
if icon:
|
||||
img.paste(icon, (cx - 48, 55), icon)
|
||||
|
||||
# Status text
|
||||
if progress.active:
|
||||
status = 'TRANSFERRING...'
|
||||
status_color = (0, 255, 100)
|
||||
elif progress.percent > 0:
|
||||
status = 'TRANSFER COMPLETE'
|
||||
status_color = (0, 200, 255)
|
||||
else:
|
||||
status = 'WAITING FOR HOST'
|
||||
status_color = (255, 200, 0)
|
||||
|
||||
bbox = draw.textbbox((0, 0), status, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, 160), status, fill=status_color, font=font_small)
|
||||
|
||||
# Progress bar (circular arc style)
|
||||
bar_y = 190
|
||||
bar_width = 280
|
||||
bar_height = 20
|
||||
bar_x = cx - bar_width // 2
|
||||
|
||||
# Background
|
||||
draw.rounded_rectangle(
|
||||
[bar_x, bar_y, bar_x + bar_width, bar_y + bar_height],
|
||||
radius=10,
|
||||
fill=(30, 30, 40)
|
||||
)
|
||||
|
||||
# Progress fill
|
||||
fill_width = int(bar_width * (progress.percent / 100))
|
||||
if fill_width > 0:
|
||||
# Gradient-like effect with orange to yellow
|
||||
draw.rounded_rectangle(
|
||||
[bar_x, bar_y, bar_x + fill_width, bar_y + bar_height],
|
||||
radius=10,
|
||||
fill=(255, 150, 0)
|
||||
)
|
||||
|
||||
# Percentage text
|
||||
pct_text = f'{progress.percent:.0f}%'
|
||||
bbox = draw.textbbox((0, 0), pct_text, font=font_large)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, 220), pct_text, fill=(255, 255, 255), font=font_large)
|
||||
|
||||
# Size info
|
||||
size_text = f'{progress.format_size(progress.bytes_read)} / {progress.format_size(progress.total_size)}'
|
||||
bbox = draw.textbbox((0, 0), size_text, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, 270), size_text, fill=TEXT_MUTED, font=font_small)
|
||||
|
||||
# Speed and ETA
|
||||
if progress.speed_mbps > 0:
|
||||
speed_text = f'{progress.speed_mbps:.1f} MB/s'
|
||||
bbox = draw.textbbox((0, 0), speed_text, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, 290), speed_text, fill=(100, 200, 100), font=font_small)
|
||||
|
||||
if progress.eta_seconds > 0:
|
||||
eta_min = progress.eta_seconds // 60
|
||||
eta_sec = progress.eta_seconds % 60
|
||||
eta_text = f'ETA: {eta_min}:{eta_sec:02d}'
|
||||
bbox = draw.textbbox((0, 0), eta_text, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, 310), eta_text, fill=TEXT_MUTED, font=font_small)
|
||||
|
||||
# Footer with instructions
|
||||
footer = 'Boot from USB to flash SecuBox'
|
||||
bbox = draw.textbbox((0, 0), footer, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, HEIGHT - 50), footer, fill=(100, 100, 120), font=font_small)
|
||||
|
||||
# CyberMind branding
|
||||
brand = 'SECUBOX EYE'
|
||||
bbox = draw.textbbox((0, 0), brand, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, HEIGHT - 30), brand, fill=(201, 168, 76), font=font_small)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
class AuthState:
|
||||
"""Manage authentication state for AUTH mode (FIDO2 security key)."""
|
||||
|
||||
def __init__(self):
|
||||
self.device_id = ''
|
||||
self.challenge = ''
|
||||
self.qr_url = ''
|
||||
self.state = 'idle' # idle, pending, approved, denied
|
||||
self.last_update = 0
|
||||
self._qr_image = None
|
||||
|
||||
# Try to load device ID
|
||||
try:
|
||||
import uuid
|
||||
machine_id_path = '/etc/machine-id'
|
||||
if os.path.exists(machine_id_path):
|
||||
with open(machine_id_path, 'r') as f:
|
||||
self.device_id = f.read().strip()[:12]
|
||||
else:
|
||||
self.device_id = uuid.uuid4().hex[:12]
|
||||
except:
|
||||
self.device_id = 'eye-remote'
|
||||
|
||||
def update(self) -> bool:
|
||||
"""Update auth state from status file.
|
||||
|
||||
Returns:
|
||||
True if state changed
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(AUTH_STATE_FILE):
|
||||
with open(AUTH_STATE_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
old_state = self.state
|
||||
self.state = data.get('state', 'idle')
|
||||
self.challenge = data.get('challenge', '')
|
||||
self.qr_url = data.get('qr_url', '')
|
||||
self.last_update = data.get('timestamp', 0)
|
||||
|
||||
# Regenerate QR if URL changed
|
||||
if self.qr_url and self.qr_url != getattr(self, '_last_qr_url', ''):
|
||||
self._generate_qr()
|
||||
self._last_qr_url = self.qr_url
|
||||
|
||||
return self.state != old_state
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _generate_qr(self):
|
||||
"""Generate QR code image from URL."""
|
||||
try:
|
||||
import qrcode
|
||||
# Create QR code with medium error correction
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=1, # ERROR_CORRECT_M = 1
|
||||
box_size=6,
|
||||
border=2,
|
||||
)
|
||||
qr.add_data(self.qr_url)
|
||||
qr.make(fit=True)
|
||||
|
||||
# Create image with PIL
|
||||
qr_img = qr.make_image(fill_color='white', back_color='black')
|
||||
|
||||
# Convert to PIL Image - handle different qrcode versions
|
||||
try:
|
||||
# Try PIL image factory (qrcode >= 7.0)
|
||||
if hasattr(qr_img, '_img'):
|
||||
pil_img = qr_img._img
|
||||
elif hasattr(qr_img, 'get_image'):
|
||||
pil_img = qr_img.get_image()
|
||||
else:
|
||||
# Fallback: convert via tobytes/frombytes
|
||||
pil_img = qr_img
|
||||
except Exception:
|
||||
pil_img = qr_img
|
||||
|
||||
# Ensure it's an RGBA image at the right size
|
||||
if hasattr(pil_img, 'convert'):
|
||||
self._qr_image = pil_img.convert('RGBA').resize(
|
||||
(AUTH_QR_SIZE, AUTH_QR_SIZE),
|
||||
Image.Resampling.NEAREST if hasattr(Image, 'Resampling') else Image.NEAREST
|
||||
)
|
||||
else:
|
||||
# Last resort: create blank image
|
||||
self._qr_image = Image.new('RGBA', (AUTH_QR_SIZE, AUTH_QR_SIZE), (255, 255, 255, 255))
|
||||
|
||||
except ImportError:
|
||||
print('qrcode library not installed')
|
||||
self._qr_image = None
|
||||
except Exception as e:
|
||||
print(f'QR generation error: {e}')
|
||||
self._qr_image = None
|
||||
|
||||
def get_qr_image(self):
|
||||
"""Get the QR code image.
|
||||
|
||||
Returns:
|
||||
PIL Image or None if not generated
|
||||
"""
|
||||
return self._qr_image
|
||||
|
||||
def generate_backup_code(self) -> str:
|
||||
"""Generate a backup authentication URL for QR code.
|
||||
|
||||
Uses device ID and a timestamp-based challenge.
|
||||
"""
|
||||
import hashlib
|
||||
ts = int(time.time())
|
||||
data = f'{self.device_id}:{ts}'
|
||||
challenge = hashlib.sha256(data.encode()).hexdigest()[:16]
|
||||
self.challenge = challenge
|
||||
# Format: secubox-auth://device_id/challenge
|
||||
self.qr_url = f'secubox-auth://{self.device_id}/{challenge}'
|
||||
self._generate_qr()
|
||||
return self.qr_url
|
||||
|
||||
|
||||
def draw_auth_mode(auth: AuthState) -> Image.Image:
|
||||
"""Draw auth mode screen with QR code.
|
||||
|
||||
Args:
|
||||
auth: AuthState instance
|
||||
|
||||
Returns:
|
||||
PIL Image ready for framebuffer
|
||||
"""
|
||||
img = Image.new('RGBA', (WIDTH, HEIGHT), BG_COLOR + (255,))
|
||||
draw = ImageDraw.Draw(img)
|
||||
cx, cy = WIDTH // 2, HEIGHT // 2
|
||||
|
||||
# Load fonts
|
||||
try:
|
||||
font_large = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 24)
|
||||
font_medium = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 14)
|
||||
font_small = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 11)
|
||||
except:
|
||||
font_large = ImageFont.load_default()
|
||||
font_medium = font_large
|
||||
font_small = font_large
|
||||
|
||||
# Draw circular border
|
||||
draw.ellipse([10, 10, WIDTH-10, HEIGHT-10], outline=(40, 40, 50), width=2)
|
||||
|
||||
# Header
|
||||
header = 'AUTH MODE'
|
||||
bbox = draw.textbbox((0, 0), header, font=font_medium)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, 20), header, fill=(192, 78, 36), font=font_medium)
|
||||
|
||||
# Auth icon
|
||||
icon = load_module_icon('AUTH', 48)
|
||||
if icon:
|
||||
img.paste(icon, (cx - 24, 45), icon)
|
||||
|
||||
# State indicator
|
||||
state_colors = {
|
||||
'idle': (100, 100, 120),
|
||||
'pending': (255, 200, 0),
|
||||
'approved': (0, 255, 100),
|
||||
'denied': (255, 50, 50),
|
||||
}
|
||||
state_texts = {
|
||||
'idle': 'READY TO AUTHENTICATE',
|
||||
'pending': 'TOUCH TO APPROVE',
|
||||
'approved': 'AUTHENTICATED',
|
||||
'denied': 'ACCESS DENIED',
|
||||
}
|
||||
|
||||
state_color = state_colors.get(auth.state, state_colors['idle'])
|
||||
state_text = state_texts.get(auth.state, 'UNKNOWN')
|
||||
|
||||
bbox = draw.textbbox((0, 0), state_text, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, 100), state_text, fill=state_color, font=font_small)
|
||||
|
||||
# QR code (centered, below status)
|
||||
qr_img = auth.get_qr_image()
|
||||
if qr_img:
|
||||
qr_x = cx - AUTH_QR_SIZE // 2
|
||||
qr_y = 125
|
||||
img.paste(qr_img, (qr_x, qr_y), qr_img)
|
||||
|
||||
# QR label
|
||||
qr_label = 'BACKUP CODE'
|
||||
bbox = draw.textbbox((0, 0), qr_label, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, qr_y + AUTH_QR_SIZE + 5), qr_label, fill=TEXT_MUTED, font=font_small)
|
||||
else:
|
||||
# No QR - show placeholder
|
||||
draw.rectangle(
|
||||
[cx - AUTH_QR_SIZE//2, 125, cx + AUTH_QR_SIZE//2, 125 + AUTH_QR_SIZE],
|
||||
outline=(50, 50, 60),
|
||||
width=2
|
||||
)
|
||||
placeholder = 'QR CODE'
|
||||
bbox = draw.textbbox((0, 0), placeholder, font=font_medium)
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
draw.text((cx - tw//2, 125 + AUTH_QR_SIZE//2 - th//2), placeholder, fill=(60, 60, 70), font=font_medium)
|
||||
|
||||
# Device ID
|
||||
device_text = f'Device: {auth.device_id}'
|
||||
bbox = draw.textbbox((0, 0), device_text, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, HEIGHT - 80), device_text, fill=TEXT_MUTED, font=font_small)
|
||||
|
||||
# Instructions
|
||||
if auth.state == 'pending':
|
||||
instr = 'Touch the display to approve'
|
||||
else:
|
||||
instr = 'Connect USB for FIDO2 auth'
|
||||
|
||||
bbox = draw.textbbox((0, 0), instr, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, HEIGHT - 55), instr, fill=(100, 100, 120), font=font_small)
|
||||
|
||||
# Branding
|
||||
brand = 'SECUBOX EYE'
|
||||
bbox = draw.textbbox((0, 0), brand, font=font_small)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text((cx - tw//2, HEIGHT - 30), brand, fill=(201, 168, 76), font=font_small)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def draw_dashboard(metrics, mode='SIM', host='', device_name=''):
|
||||
"""Draw the dashboard to an image
|
||||
|
||||
|
|
@ -581,6 +1223,10 @@ def main():
|
|||
else:
|
||||
print(f'WARNING: {FB_DEV} not found!')
|
||||
|
||||
# Check initial gadget mode
|
||||
gadget_mode = get_gadget_mode()
|
||||
print(f'Gadget mode: {gadget_mode}')
|
||||
|
||||
# Try agent first, fall back to simulation
|
||||
if os.path.exists(AGENT_SOCKET):
|
||||
print(f'Eye Agent socket found: {AGENT_SOCKET}')
|
||||
|
|
@ -588,27 +1234,123 @@ def main():
|
|||
print('Agent not running, will use simulation mode')
|
||||
|
||||
source = AgentMetricsSource()
|
||||
terminal = None
|
||||
flash_progress = None
|
||||
|
||||
# Initialize serial terminal for TTY mode
|
||||
if gadget_mode == 'tty':
|
||||
terminal = SerialTerminal()
|
||||
if terminal.open():
|
||||
print(f'Serial terminal opened: {SERIAL_DEV}')
|
||||
else:
|
||||
print('Serial terminal failed to open, using simulation')
|
||||
# Add some simulated lines for testing
|
||||
terminal.add_simulated_line('U-Boot 2024.01 (Apr 15 2026)')
|
||||
terminal.add_simulated_line('')
|
||||
terminal.add_simulated_line('Marvell ARMADA 3720 ESPRESSOBIN Board')
|
||||
terminal.add_simulated_line('')
|
||||
terminal.add_simulated_line('DRAM: 1 GiB')
|
||||
terminal.add_simulated_line('SF: Detected w25q32dw with page size 256 Bytes')
|
||||
terminal.add_simulated_line('Net: eth0: mvpp2-0, eth1: mvpp2-1')
|
||||
terminal.add_simulated_line('')
|
||||
terminal.add_simulated_line('Hit any key to stop autoboot: 0')
|
||||
terminal.add_simulated_line('=> ')
|
||||
|
||||
# Initialize flash progress for FLASH mode
|
||||
if gadget_mode == 'flash':
|
||||
flash_progress = FlashProgress()
|
||||
print(f'Flash mode initialized, image size: {flash_progress.format_size(flash_progress.total_size)}')
|
||||
|
||||
# Initialize auth state for AUTH mode
|
||||
auth_state = None
|
||||
if gadget_mode == 'auth':
|
||||
auth_state = AuthState()
|
||||
auth_state.generate_backup_code()
|
||||
print(f'Auth mode initialized, device: {auth_state.device_id}')
|
||||
|
||||
# Draw initial frame immediately
|
||||
print('Drawing initial frame...')
|
||||
try:
|
||||
metrics, mode, host, device_name = source.get_metrics()
|
||||
img = draw_dashboard(metrics, mode, host, device_name)
|
||||
if gadget_mode == 'tty' and terminal:
|
||||
img = draw_terminal(terminal, gadget_mode)
|
||||
elif gadget_mode == 'flash' and flash_progress:
|
||||
img = draw_flash_progress(flash_progress)
|
||||
elif gadget_mode == 'auth':
|
||||
if not auth_state:
|
||||
auth_state = AuthState()
|
||||
auth_state.generate_backup_code()
|
||||
img = draw_auth_mode(auth_state)
|
||||
else:
|
||||
metrics, mode, host, device_name = source.get_metrics()
|
||||
img = draw_dashboard(metrics, mode, host, device_name)
|
||||
write_to_fb(img)
|
||||
print(f'Initial frame drawn, mode={mode}')
|
||||
print(f'Initial frame drawn, gadget_mode={gadget_mode}')
|
||||
except Exception as e:
|
||||
print(f'Initial frame error: {e}')
|
||||
|
||||
last_gadget_mode = gadget_mode
|
||||
|
||||
while True:
|
||||
try:
|
||||
metrics, mode, host, device_name = source.get_metrics()
|
||||
img = draw_dashboard(metrics, mode, host, device_name)
|
||||
write_to_fb(img)
|
||||
# Check for mode changes
|
||||
gadget_mode = get_gadget_mode()
|
||||
if gadget_mode != last_gadget_mode:
|
||||
print(f'Mode changed: {last_gadget_mode} -> {gadget_mode}')
|
||||
last_gadget_mode = gadget_mode
|
||||
|
||||
# Handle terminal lifecycle
|
||||
if gadget_mode == 'tty':
|
||||
if not terminal:
|
||||
terminal = SerialTerminal()
|
||||
if not terminal.serial:
|
||||
terminal.open()
|
||||
elif terminal and terminal.serial:
|
||||
terminal.close()
|
||||
|
||||
# Handle flash progress lifecycle
|
||||
if gadget_mode == 'flash':
|
||||
if not flash_progress:
|
||||
flash_progress = FlashProgress()
|
||||
print(f'Flash mode initialized')
|
||||
|
||||
# Handle auth state lifecycle
|
||||
if gadget_mode == 'auth':
|
||||
if not auth_state:
|
||||
auth_state = AuthState()
|
||||
auth_state.generate_backup_code()
|
||||
print(f'Auth mode initialized')
|
||||
|
||||
# Render based on current mode
|
||||
if gadget_mode == 'tty' and terminal:
|
||||
# Read any new serial data
|
||||
terminal.read()
|
||||
img = draw_terminal(terminal, gadget_mode)
|
||||
write_to_fb(img)
|
||||
time.sleep(0.1) # Faster refresh for serial data
|
||||
elif gadget_mode == 'flash':
|
||||
# Flash mode with progress
|
||||
if not flash_progress:
|
||||
flash_progress = FlashProgress()
|
||||
flash_progress.update()
|
||||
img = draw_flash_progress(flash_progress)
|
||||
write_to_fb(img)
|
||||
time.sleep(0.5) # Medium refresh for progress updates
|
||||
elif gadget_mode == 'auth':
|
||||
# Auth mode with QR code
|
||||
if not auth_state:
|
||||
auth_state = AuthState()
|
||||
auth_state.generate_backup_code()
|
||||
auth_state.update()
|
||||
img = draw_auth_mode(auth_state)
|
||||
write_to_fb(img)
|
||||
time.sleep(0.5) # Medium refresh for auth updates
|
||||
else:
|
||||
# Standard dashboard mode
|
||||
metrics, mode, host, device_name = source.get_metrics()
|
||||
img = draw_dashboard(metrics, mode, host, device_name)
|
||||
write_to_fb(img)
|
||||
time.sleep(1)
|
||||
|
||||
# Log connection changes
|
||||
if mode != 'SIM' and host:
|
||||
pass # Connected to real device
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print('\nExiting...')
|
||||
break
|
||||
|
|
@ -616,6 +1358,10 @@ def main():
|
|||
print(f'Error: {e}')
|
||||
time.sleep(1)
|
||||
|
||||
# Cleanup
|
||||
if terminal:
|
||||
terminal.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
|||
722
tools/secubox-gen-gui/main.py
Normal file
722
tools/secubox-gen-gui/main.py
Normal file
|
|
@ -0,0 +1,722 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
SecuBox Profile Generator — GUI Application
|
||||
============================================
|
||||
|
||||
Multiplatform GUI for generating SecuBox appliance profiles.
|
||||
Can be launched via USB autorun on Windows/Mac/Linux.
|
||||
|
||||
CyberMind — https://cybermind.fr
|
||||
Author: Gérald Kerma <gandalf@gk2.net>
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, List
|
||||
from enum import Enum
|
||||
|
||||
# Try PyQt6 first, fall back to tkinter
|
||||
try:
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QPushButton, QComboBox, QCheckBox, QGroupBox, QTabWidget,
|
||||
QListWidget, QListWidgetItem, QProgressBar, QTextEdit, QFileDialog,
|
||||
QMessageBox, QFrame, QSplitter, QScrollArea, QGridLayout, QSpacerItem,
|
||||
QSizePolicy, QStackedWidget
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
||||
from PyQt6.QtGui import QFont, QColor, QPalette, QIcon, QPixmap
|
||||
USE_QT = True
|
||||
except ImportError:
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
USE_QT = False
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Data Models (from profile-generator.md v0.2)
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class Tier(Enum):
|
||||
LITE = "lite" # ≤1GB RAM, ≤2 cores
|
||||
STANDARD = "standard" # 2-4GB RAM, 2-4 cores
|
||||
PRO = "pro" # 4GB+ RAM, 4+ cores
|
||||
|
||||
|
||||
class ModuleLevel(Enum):
|
||||
DISABLED = "disabled"
|
||||
MINIMAL = "minimal"
|
||||
STANDARD = "standard"
|
||||
FULL = "full"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Board:
|
||||
id: str
|
||||
name: str
|
||||
arch: str
|
||||
min_tier: Tier
|
||||
ram_mb: int
|
||||
cores: int
|
||||
description: str
|
||||
icon: str = "🖥️"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Module:
|
||||
id: str
|
||||
name: str
|
||||
color: str # Light 3 palette
|
||||
zkp_layer: str # L1, L2, L3, or "-"
|
||||
description: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Profile:
|
||||
tier: Tier
|
||||
board: Board
|
||||
stage: str = "dev" # dev / staging / cspn-frozen
|
||||
modules: Dict[str, ModuleLevel] = field(default_factory=dict)
|
||||
tweaks: List[str] = field(default_factory=list)
|
||||
components: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Static Data
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BOARDS = [
|
||||
Board("rpi-zero-w", "Raspberry Pi Zero W", "arm", Tier.LITE, 512, 1,
|
||||
"Minimal USB gadget device", "🍓"),
|
||||
Board("espressobin-v7", "ESPRESSObin v7", "arm64", Tier.LITE, 1024, 2,
|
||||
"Compact network appliance", "☕"),
|
||||
Board("rpi4", "Raspberry Pi 4", "arm64", Tier.STANDARD, 4096, 4,
|
||||
"Versatile SBC", "🍓"),
|
||||
Board("vm-x64", "Virtual Machine x64", "amd64", Tier.STANDARD, 2048, 2,
|
||||
"VirtualBox/QEMU/VMware", "💻"),
|
||||
Board("mochabin", "MOCHAbin", "arm64", Tier.PRO, 8192, 4,
|
||||
"High-performance network appliance", "☕"),
|
||||
Board("bare-metal", "Bare Metal x64", "amd64", Tier.PRO, 16384, 8,
|
||||
"Server / Desktop hardware", "🖥️"),
|
||||
]
|
||||
|
||||
MODULES = [
|
||||
Module("auth", "AUTH", "#C04E24", "L1", "NIZK Hamiltonian, G rotation 24h, PFS"),
|
||||
Module("wall", "WALL", "#9A6010", "-", "nftables, CrowdSec, rate limiting"),
|
||||
Module("boot", "BOOT", "#803018", "-", "secure boot, LUKS, dm-verity"),
|
||||
Module("mind", "MIND", "#3D35A0", "L2", "nDPId, mitmproxy bridge, DPI"),
|
||||
Module("root", "ROOT", "#0A5840", "-", "base Debian, kernel, systemd"),
|
||||
Module("mesh", "MESH", "#104A88", "L3", "WireGuard, Tailscale, MirrorNet"),
|
||||
]
|
||||
|
||||
STAGES = [
|
||||
("dev", "Development", "Unrestricted, all debug enabled"),
|
||||
("staging", "Staging", "Production-like, logs enabled"),
|
||||
("cspn-frozen", "CSPN Frozen", "Certified config, no modifications"),
|
||||
]
|
||||
|
||||
COMPONENTS = {
|
||||
"security": [
|
||||
("secubox-crowdsec", "CrowdSec IDS/IPS", True),
|
||||
("secubox-suricata", "Suricata IDS", False),
|
||||
("secubox-nftables", "nftables Firewall", True),
|
||||
("secubox-fail2ban", "Fail2ban", False),
|
||||
],
|
||||
"network": [
|
||||
("secubox-wireguard", "WireGuard VPN", True),
|
||||
("secubox-tailscale", "Tailscale Mesh", False),
|
||||
("secubox-unbound", "Unbound DNS", True),
|
||||
("secubox-haproxy", "HAProxy LB", True),
|
||||
],
|
||||
"monitoring": [
|
||||
("secubox-netdata", "Netdata", True),
|
||||
("secubox-prometheus", "Prometheus", False),
|
||||
("secubox-grafana", "Grafana", False),
|
||||
],
|
||||
"services": [
|
||||
("secubox-mitmproxy", "mitmproxy WAF", True),
|
||||
("secubox-nginx", "Nginx", True),
|
||||
("secubox-squid", "Squid Cache", False),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# SecuBox Color Palette (SPIRITUALCEPT Light 3)
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
COLORS = {
|
||||
"bg_main": "#f5f0e8", # Warm parchment
|
||||
"bg_panel": "#ebe5d9", # Panel background
|
||||
"bg_card": "#ffffff", # Card background
|
||||
"text_primary": "#2d2a24", # Dark brown text
|
||||
"text_secondary": "#5c564a", # Muted text
|
||||
"accent_gold": "#c9a84c", # Hermetic gold
|
||||
"accent_red": "#c04e24", # AUTH red
|
||||
"border": "#d4cfc3", # Subtle border
|
||||
"success": "#0a5840", # ROOT green
|
||||
"warning": "#9a6010", # WALL amber
|
||||
"error": "#803018", # BOOT rust
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# PyQt6 GUI Implementation
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if USE_QT:
|
||||
class ModuleCard(QFrame):
|
||||
"""Card widget for a single module with level selector."""
|
||||
|
||||
def __init__(self, module: Module, parent=None):
|
||||
super().__init__(parent)
|
||||
self.module = module
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
self.setStyleSheet(f"""
|
||||
QFrame {{
|
||||
background-color: {COLORS['bg_card']};
|
||||
border: 2px solid {self.module.color};
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# Header with module name and ZKP layer
|
||||
header = QHBoxLayout()
|
||||
|
||||
name_label = QLabel(f"<b>{self.module.name}</b>")
|
||||
name_label.setStyleSheet(f"color: {self.module.color}; font-size: 14px;")
|
||||
header.addWidget(name_label)
|
||||
|
||||
if self.module.zkp_layer != "-":
|
||||
layer_label = QLabel(f"[{self.module.zkp_layer}]")
|
||||
layer_label.setStyleSheet(f"color: {COLORS['text_secondary']}; font-size: 12px;")
|
||||
header.addWidget(layer_label)
|
||||
|
||||
header.addStretch()
|
||||
layout.addLayout(header)
|
||||
|
||||
# Description
|
||||
desc_label = QLabel(self.module.description)
|
||||
desc_label.setWordWrap(True)
|
||||
desc_label.setStyleSheet(f"color: {COLORS['text_secondary']}; font-size: 11px;")
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
# Level selector
|
||||
self.level_combo = QComboBox()
|
||||
for level in ModuleLevel:
|
||||
self.level_combo.addItem(level.value.capitalize(), level)
|
||||
self.level_combo.setCurrentText("Standard")
|
||||
layout.addWidget(self.level_combo)
|
||||
|
||||
def get_level(self) -> ModuleLevel:
|
||||
return self.level_combo.currentData()
|
||||
|
||||
|
||||
class BoardSelector(QWidget):
|
||||
"""Board selection widget with filtering by tier."""
|
||||
|
||||
board_changed = pyqtSignal(Board)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Tier filter
|
||||
filter_layout = QHBoxLayout()
|
||||
filter_layout.addWidget(QLabel("Tier:"))
|
||||
|
||||
self.tier_combo = QComboBox()
|
||||
self.tier_combo.addItem("All", None)
|
||||
for tier in Tier:
|
||||
self.tier_combo.addItem(tier.value.capitalize(), tier)
|
||||
self.tier_combo.currentIndexChanged.connect(self.filter_boards)
|
||||
filter_layout.addWidget(self.tier_combo)
|
||||
filter_layout.addStretch()
|
||||
layout.addLayout(filter_layout)
|
||||
|
||||
# Board list
|
||||
self.board_list = QListWidget()
|
||||
self.board_list.setStyleSheet(f"""
|
||||
QListWidget {{
|
||||
background-color: {COLORS['bg_card']};
|
||||
border: 1px solid {COLORS['border']};
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QListWidget::item {{
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid {COLORS['border']};
|
||||
}}
|
||||
QListWidget::item:selected {{
|
||||
background-color: {COLORS['accent_gold']};
|
||||
color: white;
|
||||
}}
|
||||
""")
|
||||
self.board_list.itemClicked.connect(self.on_board_selected)
|
||||
layout.addWidget(self.board_list)
|
||||
|
||||
self.populate_boards()
|
||||
|
||||
def populate_boards(self, tier_filter: Optional[Tier] = None):
|
||||
self.board_list.clear()
|
||||
for board in BOARDS:
|
||||
if tier_filter and board.min_tier != tier_filter:
|
||||
continue
|
||||
item = QListWidgetItem(f"{board.icon} {board.name}")
|
||||
item.setData(Qt.ItemDataRole.UserRole, board)
|
||||
item.setToolTip(f"{board.description}\nArch: {board.arch}\nRAM: {board.ram_mb}MB\nCores: {board.cores}")
|
||||
self.board_list.addItem(item)
|
||||
|
||||
def filter_boards(self):
|
||||
tier = self.tier_combo.currentData()
|
||||
self.populate_boards(tier)
|
||||
|
||||
def on_board_selected(self, item):
|
||||
board = item.data(Qt.ItemDataRole.UserRole)
|
||||
self.board_changed.emit(board)
|
||||
|
||||
def get_selected_board(self) -> Optional[Board]:
|
||||
item = self.board_list.currentItem()
|
||||
if item:
|
||||
return item.data(Qt.ItemDataRole.UserRole)
|
||||
return None
|
||||
|
||||
|
||||
class ComponentsPanel(QWidget):
|
||||
"""Component selection panel organized by category."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.checkboxes = {}
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setStyleSheet("QScrollArea { border: none; }")
|
||||
|
||||
content = QWidget()
|
||||
content_layout = QVBoxLayout(content)
|
||||
|
||||
for category, components in COMPONENTS.items():
|
||||
group = QGroupBox(category.capitalize())
|
||||
group.setStyleSheet(f"""
|
||||
QGroupBox {{
|
||||
font-weight: bold;
|
||||
border: 1px solid {COLORS['border']};
|
||||
border-radius: 4px;
|
||||
margin-top: 12px;
|
||||
padding-top: 8px;
|
||||
}}
|
||||
QGroupBox::title {{
|
||||
color: {COLORS['accent_gold']};
|
||||
}}
|
||||
""")
|
||||
group_layout = QVBoxLayout(group)
|
||||
|
||||
for comp_id, comp_name, default in components:
|
||||
cb = QCheckBox(comp_name)
|
||||
cb.setChecked(default)
|
||||
cb.setStyleSheet(f"color: {COLORS['text_primary']};")
|
||||
group_layout.addWidget(cb)
|
||||
self.checkboxes[comp_id] = cb
|
||||
|
||||
content_layout.addWidget(group)
|
||||
|
||||
content_layout.addStretch()
|
||||
scroll.setWidget(content)
|
||||
layout.addWidget(scroll)
|
||||
|
||||
def get_selected_components(self) -> List[str]:
|
||||
return [comp_id for comp_id, cb in self.checkboxes.items() if cb.isChecked()]
|
||||
|
||||
|
||||
class OutputPanel(QWidget):
|
||||
"""Panel showing generated manifest and actions."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Output format selector
|
||||
format_layout = QHBoxLayout()
|
||||
format_layout.addWidget(QLabel("Output Format:"))
|
||||
|
||||
self.format_combo = QComboBox()
|
||||
self.format_combo.addItems(["ISO Live", "VDI (VirtualBox)", "QCOW2", "Raw IMG", "Chroot Tarball"])
|
||||
format_layout.addWidget(self.format_combo)
|
||||
format_layout.addStretch()
|
||||
layout.addLayout(format_layout)
|
||||
|
||||
# Manifest preview
|
||||
preview_label = QLabel("Generated Manifest:")
|
||||
preview_label.setStyleSheet(f"color: {COLORS['text_secondary']}; font-weight: bold;")
|
||||
layout.addWidget(preview_label)
|
||||
|
||||
self.manifest_preview = QTextEdit()
|
||||
self.manifest_preview.setReadOnly(True)
|
||||
self.manifest_preview.setStyleSheet(f"""
|
||||
QTextEdit {{
|
||||
background-color: {COLORS['bg_card']};
|
||||
border: 1px solid {COLORS['border']};
|
||||
border-radius: 4px;
|
||||
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
||||
font-size: 11px;
|
||||
}}
|
||||
""")
|
||||
layout.addWidget(self.manifest_preview)
|
||||
|
||||
# Progress bar
|
||||
self.progress = QProgressBar()
|
||||
self.progress.setVisible(False)
|
||||
layout.addWidget(self.progress)
|
||||
|
||||
# Action buttons
|
||||
btn_layout = QHBoxLayout()
|
||||
|
||||
self.save_btn = QPushButton("💾 Save Manifest")
|
||||
self.save_btn.setStyleSheet(self.button_style(COLORS['accent_gold']))
|
||||
btn_layout.addWidget(self.save_btn)
|
||||
|
||||
self.build_btn = QPushButton("🔨 Build Image")
|
||||
self.build_btn.setStyleSheet(self.button_style(COLORS['success']))
|
||||
btn_layout.addWidget(self.build_btn)
|
||||
|
||||
self.fetch_btn = QPushButton("⬇️ Fetch Pre-built")
|
||||
self.fetch_btn.setStyleSheet(self.button_style(COLORS['accent_red']))
|
||||
btn_layout.addWidget(self.fetch_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def button_style(self, color: str) -> str:
|
||||
return f"""
|
||||
QPushButton {{
|
||||
background-color: {color};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 10px 20px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
opacity: 0.9;
|
||||
}}
|
||||
"""
|
||||
|
||||
def update_manifest(self, manifest: dict):
|
||||
import yaml
|
||||
try:
|
||||
text = yaml.dump(manifest, default_flow_style=False, allow_unicode=True)
|
||||
except:
|
||||
text = json.dumps(manifest, indent=2)
|
||||
self.manifest_preview.setText(text)
|
||||
|
||||
|
||||
class SecuBoxGenGUI(QMainWindow):
|
||||
"""Main application window."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.current_profile = Profile(tier=Tier.STANDARD, board=BOARDS[3]) # Default: vm-x64
|
||||
self.setup_ui()
|
||||
self.update_manifest()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle("SecuBox Profile Generator")
|
||||
self.setMinimumSize(1200, 800)
|
||||
self.setStyleSheet(f"background-color: {COLORS['bg_main']};")
|
||||
|
||||
# Central widget
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
|
||||
main_layout = QHBoxLayout(central)
|
||||
main_layout.setSpacing(16)
|
||||
main_layout.setContentsMargins(16, 16, 16, 16)
|
||||
|
||||
# Left panel: Board & Stage selection
|
||||
left_panel = QWidget()
|
||||
left_panel.setMaximumWidth(300)
|
||||
left_layout = QVBoxLayout(left_panel)
|
||||
|
||||
# Logo/Header
|
||||
header = QLabel("⬡ SecuBox Generator")
|
||||
header.setStyleSheet(f"""
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: {COLORS['accent_gold']};
|
||||
padding: 8px;
|
||||
""")
|
||||
left_layout.addWidget(header)
|
||||
|
||||
# Board selector
|
||||
board_group = QGroupBox("Target Board")
|
||||
board_group.setStyleSheet(self.group_style())
|
||||
board_layout = QVBoxLayout(board_group)
|
||||
self.board_selector = BoardSelector()
|
||||
self.board_selector.board_changed.connect(self.on_board_changed)
|
||||
board_layout.addWidget(self.board_selector)
|
||||
left_layout.addWidget(board_group)
|
||||
|
||||
# Stage selector
|
||||
stage_group = QGroupBox("Stage")
|
||||
stage_group.setStyleSheet(self.group_style())
|
||||
stage_layout = QVBoxLayout(stage_group)
|
||||
self.stage_combo = QComboBox()
|
||||
for stage_id, stage_name, stage_desc in STAGES:
|
||||
self.stage_combo.addItem(f"{stage_name}", stage_id)
|
||||
self.stage_combo.currentIndexChanged.connect(self.update_manifest)
|
||||
stage_layout.addWidget(self.stage_combo)
|
||||
left_layout.addWidget(stage_group)
|
||||
|
||||
left_layout.addStretch()
|
||||
main_layout.addWidget(left_panel)
|
||||
|
||||
# Center panel: Modules
|
||||
center_panel = QWidget()
|
||||
center_layout = QVBoxLayout(center_panel)
|
||||
|
||||
modules_label = QLabel("Modules Configuration")
|
||||
modules_label.setStyleSheet(f"font-size: 16px; font-weight: bold; color: {COLORS['text_primary']};")
|
||||
center_layout.addWidget(modules_label)
|
||||
|
||||
modules_grid = QGridLayout()
|
||||
modules_grid.setSpacing(12)
|
||||
|
||||
self.module_cards = {}
|
||||
for i, module in enumerate(MODULES):
|
||||
card = ModuleCard(module)
|
||||
card.level_combo.currentIndexChanged.connect(self.update_manifest)
|
||||
self.module_cards[module.id] = card
|
||||
modules_grid.addWidget(card, i // 3, i % 3)
|
||||
|
||||
center_layout.addLayout(modules_grid)
|
||||
|
||||
# Components panel
|
||||
components_label = QLabel("Components")
|
||||
components_label.setStyleSheet(f"font-size: 16px; font-weight: bold; color: {COLORS['text_primary']};")
|
||||
center_layout.addWidget(components_label)
|
||||
|
||||
self.components_panel = ComponentsPanel()
|
||||
center_layout.addWidget(self.components_panel)
|
||||
|
||||
main_layout.addWidget(center_panel, stretch=2)
|
||||
|
||||
# Right panel: Output
|
||||
right_panel = QWidget()
|
||||
right_panel.setMaximumWidth(400)
|
||||
right_layout = QVBoxLayout(right_panel)
|
||||
|
||||
output_label = QLabel("Output")
|
||||
output_label.setStyleSheet(f"font-size: 16px; font-weight: bold; color: {COLORS['text_primary']};")
|
||||
right_layout.addWidget(output_label)
|
||||
|
||||
self.output_panel = OutputPanel()
|
||||
self.output_panel.save_btn.clicked.connect(self.save_manifest)
|
||||
self.output_panel.build_btn.clicked.connect(self.build_image)
|
||||
self.output_panel.fetch_btn.clicked.connect(self.fetch_prebuilt)
|
||||
right_layout.addWidget(self.output_panel)
|
||||
|
||||
main_layout.addWidget(right_panel)
|
||||
|
||||
def group_style(self) -> str:
|
||||
return f"""
|
||||
QGroupBox {{
|
||||
font-weight: bold;
|
||||
border: 1px solid {COLORS['border']};
|
||||
border-radius: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background-color: {COLORS['bg_panel']};
|
||||
}}
|
||||
QGroupBox::title {{
|
||||
color: {COLORS['accent_gold']};
|
||||
subcontrol-position: top left;
|
||||
padding: 0 8px;
|
||||
}}
|
||||
"""
|
||||
|
||||
def on_board_changed(self, board: Board):
|
||||
self.current_profile.board = board
|
||||
self.current_profile.tier = board.min_tier
|
||||
self.update_manifest()
|
||||
|
||||
def update_manifest(self):
|
||||
board = self.board_selector.get_selected_board()
|
||||
if not board:
|
||||
board = BOARDS[3] # Default vm-x64
|
||||
|
||||
stage = self.stage_combo.currentData() or "dev"
|
||||
|
||||
modules = {}
|
||||
for mod_id, card in self.module_cards.items():
|
||||
modules[mod_id] = card.get_level().value
|
||||
|
||||
components = self.components_panel.get_selected_components()
|
||||
|
||||
manifest = {
|
||||
"schema_version": 1,
|
||||
"profile": {
|
||||
"tier": board.min_tier.value,
|
||||
"board": board.id,
|
||||
"stage": stage,
|
||||
},
|
||||
"modules": modules,
|
||||
"components": components,
|
||||
"tweaks": [],
|
||||
}
|
||||
|
||||
self.output_panel.update_manifest(manifest)
|
||||
|
||||
def save_manifest(self):
|
||||
filepath, _ = QFileDialog.getSaveFileName(
|
||||
self, "Save Manifest", "manifest.yaml", "YAML Files (*.yaml *.yml)"
|
||||
)
|
||||
if filepath:
|
||||
text = self.output_panel.manifest_preview.toPlainText()
|
||||
Path(filepath).write_text(text)
|
||||
QMessageBox.information(self, "Saved", f"Manifest saved to:\n{filepath}")
|
||||
|
||||
def build_image(self):
|
||||
QMessageBox.information(
|
||||
self, "Build Image",
|
||||
"This will call:\n\nsecubox-build --manifest manifest.yaml --format iso-live\n\n"
|
||||
"(Not implemented in mockup)"
|
||||
)
|
||||
|
||||
def fetch_prebuilt(self):
|
||||
board = self.board_selector.get_selected_board()
|
||||
if not board:
|
||||
QMessageBox.warning(self, "No Board", "Please select a target board first.")
|
||||
return
|
||||
|
||||
QMessageBox.information(
|
||||
self, "Fetch Pre-built",
|
||||
f"This will call:\n\nsecubox-fetch download --board {board.id}\n\n"
|
||||
"(Not implemented in mockup)"
|
||||
)
|
||||
|
||||
|
||||
def run_qt_app():
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Set application-wide font
|
||||
font = QFont("Segoe UI", 10)
|
||||
app.setFont(font)
|
||||
|
||||
window = SecuBoxGenGUI()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Tkinter Fallback Implementation (minimal)
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
else:
|
||||
def run_tk_app():
|
||||
root = tk.Tk()
|
||||
root.title("SecuBox Profile Generator")
|
||||
root.geometry("800x600")
|
||||
root.configure(bg=COLORS['bg_main'])
|
||||
|
||||
# Header
|
||||
header = tk.Label(
|
||||
root, text="⬡ SecuBox Profile Generator",
|
||||
font=("Arial", 18, "bold"),
|
||||
fg=COLORS['accent_gold'],
|
||||
bg=COLORS['bg_main']
|
||||
)
|
||||
header.pack(pady=20)
|
||||
|
||||
# Info label
|
||||
info = tk.Label(
|
||||
root,
|
||||
text="Install PyQt6 for the full GUI experience:\n\npip install PyQt6",
|
||||
font=("Arial", 12),
|
||||
fg=COLORS['text_secondary'],
|
||||
bg=COLORS['bg_main']
|
||||
)
|
||||
info.pack(pady=20)
|
||||
|
||||
# Board selector
|
||||
frame = tk.Frame(root, bg=COLORS['bg_panel'])
|
||||
frame.pack(pady=10, padx=20, fill='x')
|
||||
|
||||
tk.Label(frame, text="Target Board:", bg=COLORS['bg_panel']).pack(side='left')
|
||||
board_var = tk.StringVar(value="vm-x64")
|
||||
board_combo = ttk.Combobox(frame, textvariable=board_var, values=[b.id for b in BOARDS])
|
||||
board_combo.pack(side='left', padx=10)
|
||||
|
||||
# Generate button
|
||||
def generate():
|
||||
board = board_var.get()
|
||||
messagebox.showinfo("Generate", f"Would generate manifest for: {board}")
|
||||
|
||||
btn = tk.Button(
|
||||
root, text="Generate Manifest",
|
||||
command=generate,
|
||||
bg=COLORS['accent_gold'],
|
||||
fg='white'
|
||||
)
|
||||
btn.pack(pady=20)
|
||||
|
||||
root.mainloop()
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# USB Autorun Entry Point
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def detect_usb_mode() -> bool:
|
||||
"""Detect if running from USB drive (portable mode)."""
|
||||
exe_path = Path(sys.executable if getattr(sys, 'frozen', False) else __file__).resolve()
|
||||
|
||||
if platform.system() == "Windows":
|
||||
# Check if on removable drive
|
||||
import ctypes
|
||||
drive = str(exe_path)[:2]
|
||||
return ctypes.windll.kernel32.GetDriveTypeW(drive) == 2 # DRIVE_REMOVABLE
|
||||
else:
|
||||
# Check if path contains /media/ or /mnt/ (typical USB mount points)
|
||||
return any(p in str(exe_path) for p in ['/media/', '/mnt/', '/Volumes/'])
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
# Check for portable/USB mode
|
||||
portable = detect_usb_mode()
|
||||
if portable:
|
||||
print("Running in portable USB mode")
|
||||
# Could load config from USB drive here
|
||||
|
||||
if USE_QT:
|
||||
run_qt_app()
|
||||
else:
|
||||
run_tk_app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
156
wiki/FAQ-Troubleshooting.md
Normal file
156
wiki/FAQ-Troubleshooting.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# SecuBox FAQ & Troubleshooting
|
||||
|
||||
Quick solutions to common issues. **For the latest fixes, always check [GitHub Issues](https://github.com/CyberMind-FR/secubox-deb/issues)** - community-reported problems often have solutions there before documentation is updated.
|
||||
|
||||
---
|
||||
|
||||
## Quick Links
|
||||
|
||||
- **GitHub Issues (check here first!)**: https://github.com/CyberMind-FR/secubox-deb/issues
|
||||
- **Known Bugs Label**: https://github.com/CyberMind-FR/secubox-deb/issues?q=label%3Abug
|
||||
- **Wiki Home**: [Home](Home.md)
|
||||
|
||||
---
|
||||
|
||||
## VirtualBox Issues
|
||||
|
||||
### Kiosk doesn't start / "No usable sandbox" error
|
||||
|
||||
**Symptom**: Chromium fails with sandbox error, kiosk service keeps restarting.
|
||||
|
||||
**Solution**: The VirtualBox image includes a fix for this. If using an older image:
|
||||
|
||||
```bash
|
||||
# SSH into VM
|
||||
ssh -p 2222 root@localhost # password: secubox
|
||||
|
||||
# Add --no-sandbox flag
|
||||
echo '--disable-gpu --disable-gpu-compositing --disable-software-rasterizer --no-sandbox' > /home/secubox-kiosk/.chromium-gpu-flags
|
||||
chown secubox-kiosk:secubox-kiosk /home/secubox-kiosk/.chromium-gpu-flags
|
||||
systemctl restart secubox-kiosk
|
||||
```
|
||||
|
||||
**Related Issue**: [#34](https://github.com/CyberMind-FR/secubox-deb/issues/34)
|
||||
|
||||
### VM tries PXE boot instead of disk
|
||||
|
||||
**Symptom**: VirtualBox attempts network boot.
|
||||
|
||||
**Solution**: Disable network boot or ensure disk is first in boot order:
|
||||
```bash
|
||||
VBoxManage modifyvm "SecuBox" --boot1 disk --boot2 none --boot3 none --boot4 none
|
||||
```
|
||||
|
||||
### WebUI not accessible via port forward
|
||||
|
||||
**Symptom**: https://localhost:9443 doesn't connect.
|
||||
|
||||
**Solution**: nginx listens on port 443, not 9443. Fix port forwarding:
|
||||
```bash
|
||||
VBoxManage controlvm "SecuBox" natpf1 delete https 2>/dev/null
|
||||
VBoxManage controlvm "SecuBox" natpf1 "https,tcp,,9443,,443"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Issues
|
||||
|
||||
### Login fails with "Invalid credentials"
|
||||
|
||||
**Default credentials**:
|
||||
- **WebUI**: `admin` / `secubox` (NOT root)
|
||||
- **SSH**: `root` / `secubox`
|
||||
|
||||
### Menu/Sidebar fails to load ("Invalid menu data")
|
||||
|
||||
**Symptom**: After login, sidebar shows error, pages don't load.
|
||||
|
||||
**Cause**: FastAPI/Pydantic compatibility issue with auth module.
|
||||
|
||||
**Status**: Known issue, fix in progress. See [#34](https://github.com/CyberMind-FR/secubox-deb/issues/34)
|
||||
|
||||
**Workaround**: Use REST API directly or wait for package update.
|
||||
|
||||
---
|
||||
|
||||
## Network Issues
|
||||
|
||||
### No IP address after boot
|
||||
|
||||
**Check DHCP**:
|
||||
```bash
|
||||
# Inside SecuBox
|
||||
ip addr show
|
||||
dhclient -v enp0s3 # or appropriate interface
|
||||
```
|
||||
|
||||
**Check NetworkManager vs systemd-networkd**:
|
||||
```bash
|
||||
systemctl status NetworkManager
|
||||
systemctl status systemd-networkd
|
||||
```
|
||||
|
||||
### Bridged mode shows wrong subnet
|
||||
|
||||
**Symptom**: VM gets IP from different network (e.g., 10.x instead of 192.168.x).
|
||||
|
||||
**Solution**: Verify bridge adapter in VirtualBox settings matches your host interface.
|
||||
|
||||
---
|
||||
|
||||
## Service Issues
|
||||
|
||||
### coturn.service keeps failing
|
||||
|
||||
**Symptom**: Boot shows repeated coturn failures.
|
||||
|
||||
**Solution**: Disable if not using TURN/STUN:
|
||||
```bash
|
||||
systemctl disable coturn
|
||||
systemctl mask coturn
|
||||
```
|
||||
|
||||
### secubox-hub socket not created
|
||||
|
||||
**Symptom**: API returns 502 Bad Gateway, `/run/secubox/hub.sock` missing.
|
||||
|
||||
**Workaround**: Service was switched to TCP binding. If you have an old image:
|
||||
```bash
|
||||
# Update service to use TCP
|
||||
sed -i 's/--uds.*sock/--host 127.0.0.1 --port 8001/' /lib/systemd/system/secubox-hub.service
|
||||
systemctl daemon-reload
|
||||
systemctl restart secubox-hub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hardware-Specific Issues
|
||||
|
||||
### ESPRESSObin / MOCHAbin
|
||||
|
||||
See [Board-Specific-Notes.md](Board-Specific-Notes.md)
|
||||
|
||||
### AMD64 / Bare Metal
|
||||
|
||||
- Ensure UEFI boot mode (GPT partition table)
|
||||
- For kiosk: verify X11/DRM drivers are loaded
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
1. **Check GitHub Issues first**: https://github.com/CyberMind-FR/secubox-deb/issues
|
||||
2. **Search closed issues** for solutions already found
|
||||
3. **Create new issue** with:
|
||||
- SecuBox version (`cat /etc/secubox/version`)
|
||||
- Board type
|
||||
- Full error messages
|
||||
- Steps to reproduce
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [VirtualBox-Setup.md](VirtualBox-Setup.md)
|
||||
- [Installation-Guide.md](Installation-Guide.md)
|
||||
- [API-Reference.md](API-Reference.md)
|
||||
Loading…
Reference in New Issue
Block a user