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>
This commit is contained in:
274
web/routes/remote_monitor.py
Normal file
274
web/routes/remote_monitor.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""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})
|
||||
Reference in New Issue
Block a user