Files
autarch/web/routes/remote_monitor.py
SsSnake da53899f66 AUTARCH v1.9 — remote monitoring, SSH manager, daemon, vault, cleanup
- Add Remote Monitoring Station with PIAP device profile system
- Add SSH/SSHD manager with fail2ban integration
- Add privileged daemon architecture for safe root operations
- Add encrypted vault, HAL memory, HAL auto-analyst
- Add network security suite, module creator, codex training
- Add start.sh launcher script and GTK3 desktop launcher
- Remove Output/ build artifacts, installer files, loose docs
- Update .gitignore for runtime data and build artifacts
- Update README for v1.9 with new launch method, screenshots, and features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 06:59:06 -07:00

275 lines
9.4 KiB
Python

"""Remote Monitoring Station — load .piap device profiles and control remote radios."""
import configparser
import logging
import os
import subprocess
import threading
import time
from datetime import datetime
from pathlib import Path
from flask import Blueprint, render_template, request, jsonify
logger = logging.getLogger(__name__)
remote_monitor_bp = Blueprint('remote_monitor', __name__, url_prefix='/remote-monitor')
PIAP_DIR = Path(__file__).parent.parent.parent / 'data' / 'piap'
CAPTURE_DIR = Path(__file__).parent.parent.parent / 'data' / 'captures'
_ssh_sessions = {}
_capture_threads = {}
def _parse_piap(filepath):
"""Parse a .piap file into a dict structure."""
cfg = configparser.ConfigParser(interpolation=None)
cfg.read(filepath)
device = dict(cfg['device']) if 'device' in cfg else {}
connection = dict(cfg['connection']) if 'connection' in cfg else {}
radios = []
i = 0
while f'radio_{i}' in cfg:
radio = dict(cfg[f'radio_{i}'])
radio['index'] = i
if 'channels' in radio:
radio['channel_list'] = [c.strip() for c in radio['channels'].split(',')]
if 'modes' in radio:
radio['mode_list'] = [m.strip() for m in radio['modes'].split(',')]
radios.append(radio)
i += 1
features = dict(cfg['features']) if 'features' in cfg else {}
info_cmds = dict(cfg['info']) if 'info' in cfg else {}
return {
'device': device,
'connection': connection,
'radios': radios,
'features': features,
'info': info_cmds,
'filename': os.path.basename(filepath),
}
def _ssh_cmd(conn, cmd, timeout=15):
"""Run a command on the remote device over SSH."""
host = conn.get('host', '')
port = conn.get('port', '22')
user = conn.get('user', 'root')
auth = conn.get('auth', 'key')
key_path = conn.get('key_path', '')
password = conn.get('password', '')
ssh_timeout = conn.get('timeout', '10')
ssh_args = ['ssh', '-o', 'StrictHostKeyChecking=no', '-o', 'ConnectTimeout=' + ssh_timeout,
'-p', port]
if auth == 'key' and key_path:
ssh_args += ['-i', key_path]
ssh_args.append(f'{user}@{host}')
ssh_args.append(cmd)
try:
r = subprocess.run(ssh_args, capture_output=True, text=True, timeout=timeout)
return {'ok': r.returncode == 0, 'stdout': r.stdout.strip(), 'stderr': r.stderr.strip(), 'code': r.returncode}
except subprocess.TimeoutExpired:
return {'ok': False, 'stdout': '', 'stderr': 'timeout', 'code': -1}
except Exception as e:
return {'ok': False, 'stdout': '', 'stderr': str(e), 'code': -1}
def _expand_cmd(cmd_template, radio=None, channel=None, bssid=None, count=None, timestamp=None):
"""Replace {variables} in a command template."""
if not cmd_template:
return ''
cmd = cmd_template
if radio:
cmd = cmd.replace('{phy}', radio.get('phy', ''))
cmd = cmd.replace('{interface}', radio.get('interface', ''))
cmd = cmd.replace('{mon}', radio.get('monitor_interface', ''))
cmd = cmd.replace('{channels}', radio.get('channels', ''))
if channel:
cmd = cmd.replace('{channel}', str(channel))
elif radio:
cmd = cmd.replace('{channel}', radio.get('default_channel', '1'))
if bssid:
cmd = cmd.replace('{bssid}', bssid)
if count:
cmd = cmd.replace('{count}', str(count))
if timestamp is None:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
cmd = cmd.replace('{timestamp}', timestamp)
return cmd
# ── Routes ──────────────────────────────────────────────────────────────────
@remote_monitor_bp.route('/')
def index():
"""Main page — loads available .piap files for dropdown."""
piap_files = []
for f in sorted(PIAP_DIR.glob('*.piap')):
if f.name == 'template.piap':
continue
try:
p = _parse_piap(f)
piap_files.append({'filename': f.name, 'name': p['device'].get('name', f.stem)})
except Exception as e:
logger.warning("Failed to parse %s: %s", f, e)
return render_template('remote_monitor.html', piap_files=piap_files)
@remote_monitor_bp.route('/api/load', methods=['POST'])
def load_piap():
"""Load a .piap file and return its full config."""
filename = request.json.get('filename', '')
filepath = PIAP_DIR / filename
if not filepath.exists() or not filepath.suffix == '.piap':
return jsonify({'ok': False, 'error': 'File not found'}), 404
try:
data = _parse_piap(filepath)
return jsonify({'ok': True, 'data': data})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
@remote_monitor_bp.route('/api/connect', methods=['POST'])
def connect():
"""Test SSH connection to the remote device."""
conn = request.json.get('connection', {})
result = _ssh_cmd(conn, 'echo ok')
return jsonify(result)
@remote_monitor_bp.route('/api/info', methods=['POST'])
def device_info():
"""Get device info (uptime, memory, kernel, etc)."""
conn = request.json.get('connection', {})
info_cmds = request.json.get('info', {})
results = {}
for key, cmd in info_cmds.items():
if key.startswith('cmd_'):
label = key[4:]
r = _ssh_cmd(conn, cmd)
results[label] = r.get('stdout', r.get('stderr', ''))
return jsonify({'ok': True, 'info': results})
@remote_monitor_bp.route('/api/radio/status', methods=['POST'])
def radio_status():
"""Get status of a specific radio."""
conn = request.json.get('connection', {})
radio = request.json.get('radio', {})
cmd = _expand_cmd(radio.get('cmd_status', 'iw dev'), radio)
result = _ssh_cmd(conn, cmd)
return jsonify(result)
@remote_monitor_bp.route('/api/radio/monitor-on', methods=['POST'])
def monitor_on():
"""Enable monitor mode on a radio."""
conn = request.json.get('connection', {})
radio = request.json.get('radio', {})
channel = request.json.get('channel', radio.get('default_channel', '1'))
cmd = _expand_cmd(radio.get('cmd_monitor_on', ''), radio, channel=channel)
result = _ssh_cmd(conn, cmd)
return jsonify(result)
@remote_monitor_bp.route('/api/radio/monitor-off', methods=['POST'])
def monitor_off():
"""Disable monitor mode on a radio."""
conn = request.json.get('connection', {})
radio = request.json.get('radio', {})
cmd = _expand_cmd(radio.get('cmd_monitor_off', ''), radio)
result = _ssh_cmd(conn, cmd)
return jsonify(result)
@remote_monitor_bp.route('/api/radio/set-channel', methods=['POST'])
def set_channel():
"""Set channel on a monitor interface."""
conn = request.json.get('connection', {})
radio = request.json.get('radio', {})
channel = request.json.get('channel', '1')
cmd = _expand_cmd(radio.get('cmd_set_channel', ''), radio, channel=channel)
result = _ssh_cmd(conn, cmd)
return jsonify(result)
@remote_monitor_bp.route('/api/capture/start', methods=['POST'])
def capture_start():
"""Start packet capture on remote device."""
conn = request.json.get('connection', {})
radio = request.json.get('radio', {})
features = request.json.get('features', {})
cmd = _expand_cmd(features.get('cmd_capture_start', ''), radio)
result = _ssh_cmd(conn, cmd)
return jsonify(result)
@remote_monitor_bp.route('/api/capture/stop', methods=['POST'])
def capture_stop():
"""Stop packet capture on remote device."""
conn = request.json.get('connection', {})
features = request.json.get('features', {})
cmd = features.get('cmd_capture_stop', 'killall tcpdump 2>/dev/null')
result = _ssh_cmd(conn, cmd)
return jsonify(result)
@remote_monitor_bp.route('/api/scan', methods=['POST'])
def wifi_scan():
"""Run passive WiFi scan."""
conn = request.json.get('connection', {})
radio = request.json.get('radio', {})
features = request.json.get('features', {})
cmd = _expand_cmd(features.get('cmd_wifi_scan', ''), radio)
result = _ssh_cmd(conn, cmd, timeout=30)
return jsonify(result)
@remote_monitor_bp.route('/api/deauth', methods=['POST'])
def deauth():
"""Send deauth frames."""
conn = request.json.get('connection', {})
radio = request.json.get('radio', {})
features = request.json.get('features', {})
bssid = request.json.get('bssid', '')
count = request.json.get('count', '10')
cmd = _expand_cmd(features.get('cmd_deauth', ''), radio, bssid=bssid, count=count)
result = _ssh_cmd(conn, cmd, timeout=30)
return jsonify(result)
@remote_monitor_bp.route('/api/exec', methods=['POST'])
def exec_cmd():
"""Execute an arbitrary command on the remote device."""
conn = request.json.get('connection', {})
radio = request.json.get('radio')
cmd = request.json.get('cmd', '')
if radio:
cmd = _expand_cmd(cmd, radio)
result = _ssh_cmd(conn, cmd, timeout=30)
return jsonify(result)
@remote_monitor_bp.route('/api/piap/list')
def list_piaps():
"""List available .piap files."""
piap_files = []
for f in sorted(PIAP_DIR.glob('*.piap')):
if f.name == 'template.piap':
continue
try:
p = _parse_piap(f)
piap_files.append({'filename': f.name, 'name': p['device'].get('name', f.stem)})
except:
pass
return jsonify({'ok': True, 'files': piap_files})