From c88cf831fc0b52166f977878564ad0f57d8c1509 Mon Sep 17 00:00:00 2001 From: Smittix Date: Tue, 20 Jan 2026 18:07:33 +0000 Subject: [PATCH] Add verbose results option to TSCM sweeps and setup improvements - Add verbose_results flag to store full device details in sweep results - Add non-interactive mode (--non-interactive) to setup.sh - Add ask_yes_no helper for interactive prompts with TTY detection - Update reports.py to handle new results structure with fallbacks Co-Authored-By: Claude Opus 4.5 --- routes/tscm.py | 142 +++++++++++++-------- setup.sh | 198 +++++++++++++++++++++++------ templates/index.html | 6 +- templates/partials/modes/tscm.html | 9 ++ utils/tscm/reports.py | 30 +++-- 5 files changed, 288 insertions(+), 97 deletions(-) diff --git a/routes/tscm.py b/routes/tscm.py index 3649f30..66a974b 100644 --- a/routes/tscm.py +++ b/routes/tscm.py @@ -298,19 +298,20 @@ def _check_available_devices(wifi: bool, bt: bool, rf: bool) -> dict: @tscm_bp.route('/sweep/start', methods=['POST']) -def start_sweep(): - """Start a TSCM sweep.""" - global _sweep_running, _sweep_thread, _current_sweep_id - - if _sweep_running: +def start_sweep(): + """Start a TSCM sweep.""" + global _sweep_running, _sweep_thread, _current_sweep_id + + if _sweep_running: return jsonify({'status': 'error', 'message': 'Sweep already running'}) data = request.get_json() or {} sweep_type = data.get('sweep_type', 'standard') - baseline_id = data.get('baseline_id') - wifi_enabled = data.get('wifi', True) - bt_enabled = data.get('bluetooth', True) - rf_enabled = data.get('rf', True) + baseline_id = data.get('baseline_id') + wifi_enabled = data.get('wifi', True) + bt_enabled = data.get('bluetooth', True) + rf_enabled = data.get('rf', True) + verbose_results = bool(data.get('verbose_results', False)) # Get interface selections wifi_interface = data.get('wifi_interface', '') @@ -348,12 +349,12 @@ def start_sweep(): _sweep_running = True # Start sweep thread - _sweep_thread = threading.Thread( - target=_run_sweep, - args=(sweep_type, baseline_id, wifi_enabled, bt_enabled, rf_enabled, - wifi_interface, bt_interface, sdr_device), - daemon=True - ) + _sweep_thread = threading.Thread( + target=_run_sweep, + args=(sweep_type, baseline_id, wifi_enabled, bt_enabled, rf_enabled, + wifi_interface, bt_interface, sdr_device, verbose_results), + daemon=True + ) _sweep_thread.start() logger.info(f"Started TSCM sweep: type={sweep_type}, id={_current_sweep_id}") @@ -1193,16 +1194,17 @@ def _scan_rf_signals(sdr_device: int | None, duration: int = 30) -> list[dict]: return signals -def _run_sweep( - sweep_type: str, - baseline_id: int | None, - wifi_enabled: bool, - bt_enabled: bool, - rf_enabled: bool, - wifi_interface: str = '', - bt_interface: str = '', - sdr_device: int | None = None -) -> None: +def _run_sweep( + sweep_type: str, + baseline_id: int | None, + wifi_enabled: bool, + bt_enabled: bool, + rf_enabled: bool, + wifi_interface: str = '', + bt_interface: str = '', + sdr_device: int | None = None, + verbose_results: bool = False +) -> None: """ Run the TSCM sweep in a background thread. @@ -1470,21 +1472,61 @@ def _run_sweep( identity_summary = identity_engine.get_summary() identity_clusters = [c.to_dict() for c in identity_engine.get_clusters()] - update_tscm_sweep( - _current_sweep_id, - status='completed', - results={ - 'wifi_devices': len(all_wifi), - 'bt_devices': len(all_bt), - 'rf_signals': len(all_rf), - 'severity_counts': severity_counts, - 'correlation_summary': findings.get('summary', {}), - 'identity_summary': identity_summary.get('statistics', {}), - 'baseline_comparison': baseline_comparison, - }, - threats_found=threats_found, - completed=True - ) + if verbose_results: + wifi_payload = list(all_wifi.values()) + bt_payload = list(all_bt.values()) + rf_payload = list(all_rf) + else: + wifi_payload = [ + { + 'bssid': d.get('bssid') or d.get('mac'), + 'essid': d.get('essid') or d.get('ssid'), + 'ssid': d.get('ssid') or d.get('essid'), + 'channel': d.get('channel'), + 'power': d.get('power', d.get('signal')), + 'privacy': d.get('privacy', d.get('encryption')), + 'encryption': d.get('encryption', d.get('privacy')), + } + for d in all_wifi.values() + ] + bt_payload = [ + { + 'mac': d.get('mac') or d.get('address'), + 'name': d.get('name'), + 'rssi': d.get('rssi'), + 'manufacturer': d.get('manufacturer', d.get('manufacturer_name')), + } + for d in all_bt.values() + ] + rf_payload = [ + { + 'frequency': s.get('frequency'), + 'power': s.get('power', s.get('level')), + 'modulation': s.get('modulation'), + 'band': s.get('band'), + } + for s in all_rf + ] + + update_tscm_sweep( + _current_sweep_id, + status='completed', + results={ + 'wifi_devices': wifi_payload, + 'bt_devices': bt_payload, + 'rf_signals': rf_payload, + 'wifi_count': len(all_wifi), + 'bt_count': len(all_bt), + 'rf_count': len(all_rf), + 'severity_counts': severity_counts, + 'correlation_summary': findings.get('summary', {}), + 'identity_summary': identity_summary.get('statistics', {}), + 'baseline_comparison': baseline_comparison, + 'results_detail_level': 'full' if verbose_results else 'compact', + }, + threats_found=threats_found, + completed=True + ) # Emit correlation findings _emit_event('correlation_findings', { @@ -1506,13 +1548,13 @@ def _run_sweep( }) # Emit device identity cluster findings (MAC-randomization resistant) - _emit_event('identity_clusters', { - 'total_clusters': identity_summary['statistics'].get('total_clusters', 0), - 'high_risk_count': identity_summary['statistics'].get('high_risk_count', 0), - 'medium_risk_count': identity_summary['statistics'].get('medium_risk_count', 0), - 'unique_fingerprints': identity_summary['statistics'].get('unique_fingerprints', 0), - 'clusters': identity_clusters, - }) + _emit_event('identity_clusters', { + 'total_clusters': identity_summary.get('statistics', {}).get('total_clusters', 0), + 'high_risk_count': identity_summary.get('statistics', {}).get('high_risk_count', 0), + 'medium_risk_count': identity_summary.get('statistics', {}).get('medium_risk_count', 0), + 'unique_fingerprints': identity_summary.get('statistics', {}).get('unique_fingerprints', 0), + 'clusters': identity_clusters, + }) _emit_event('sweep_completed', { 'sweep_id': _current_sweep_id, @@ -2423,9 +2465,9 @@ def get_baseline_diff(baseline_id: int, sweep_id: int): import json results = json.loads(results) - current_wifi = results.get('wifi', []) - current_bt = results.get('bluetooth', []) - current_rf = results.get('rf', []) + current_wifi = results.get('wifi_devices', []) + current_bt = results.get('bt_devices', []) + current_rf = results.get('rf_signals', []) diff = calculate_baseline_diff( baseline=baseline, diff --git a/setup.sh b/setup.sh index f378624..f9ebe5b 100755 --- a/setup.sh +++ b/setup.sh @@ -69,6 +69,18 @@ echo # ---------------------------- # Helpers # ---------------------------- +NON_INTERACTIVE=false + +for arg in "$@"; do + case "$arg" in + --non-interactive) + NON_INTERACTIVE=true + ;; + *) + ;; + esac +done + cmd_exists() { local c="$1" command -v "$c" >/dev/null 2>&1 && return 0 @@ -76,6 +88,32 @@ cmd_exists() { return 1 } +ask_yes_no() { + local prompt="$1" + local default="${2:-n}" # default to no for safety + local response + + if $NON_INTERACTIVE; then + info "Non-interactive mode: defaulting to ${default} for prompt: ${prompt}" + [[ "$default" == "y" ]] + return + fi + + if [[ ! -t 0 ]]; then + warn "No TTY available for prompt: ${prompt}" + [[ "$default" == "y" ]] + return + fi + + if [[ "$default" == "y" ]]; then + read -r -p "$prompt [Y/n]: " response + [[ -z "$response" || "$response" =~ ^[Yy] ]] + else + read -r -p "$prompt [y/N]: " response + [[ "$response" =~ ^[Yy] ]] + fi +} + have_any() { local c for c in "$@"; do @@ -111,6 +149,18 @@ detect_os() { [[ "$OS" != "unknown" ]] || { fail "Unsupported OS (macOS + Debian/Ubuntu only)."; exit 1; } } +detect_dragonos() { + IS_DRAGONOS=false + # Check for DragonOS markers + if [[ -f /etc/dragonos-release ]] || \ + [[ -d /usr/share/dragonos ]] || \ + grep -qi "dragonos" /etc/os-release 2>/dev/null; then + IS_DRAGONOS=true + warn "DragonOS detected! This distro has many tools pre-installed." + warn "The script will prompt before making system changes." + fi +} + # ---------------------------- # Required tool checks (with alternates) # ---------------------------- @@ -382,6 +432,15 @@ apt_try_install_any() { return 1 } +apt_install_if_missing() { + local pkg="$1" + if dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"; then + ok "apt: ${pkg} already installed" + return 0 + fi + apt_install "$pkg" +} + install_dump1090_from_source_debian() { info "dump1090 not available via APT. Building from source (required)..." @@ -543,9 +602,17 @@ EOF install_debian_packages() { need_sudo - # Suppress needrestart prompts (Ubuntu Server 22.04+) - export DEBIAN_FRONTEND=noninteractive - export NEEDRESTART_MODE=a + # Keep APT interactive when a TTY is available. + if $NON_INTERACTIVE; then + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + elif [[ -t 0 ]]; then + export DEBIAN_FRONTEND=readline + export NEEDRESTART_MODE=a + else + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + fi TOTAL_STEPS=18 CURRENT_STEP=0 @@ -554,42 +621,59 @@ install_debian_packages() { $SUDO apt-get update -y >/dev/null progress "Installing RTL-SDR" - # Handle package conflict between librtlsdr0 and librtlsdr2 - # The newer librtlsdr0 (2.0.2) conflicts with older librtlsdr2 (2.0.1) - if dpkg -l | grep -q "librtlsdr2"; then - info "Detected librtlsdr2 conflict - upgrading to librtlsdr0..." + if ! $IS_DRAGONOS; then + # Handle package conflict between librtlsdr0 and librtlsdr2 + # The newer librtlsdr0 (2.0.2) conflicts with older librtlsdr2 (2.0.1) + if dpkg -l | grep -q "librtlsdr2"; then + info "Detected librtlsdr2 conflict - upgrading to librtlsdr0..." - # Remove packages that depend on librtlsdr2, then remove librtlsdr2 - # These will be reinstalled with librtlsdr0 support - $SUDO apt-get remove -y dump1090-mutability libgnuradio-osmosdr0.2.0t64 rtl-433 librtlsdr2 rtl-sdr 2>/dev/null || true - $SUDO apt-get autoremove -y 2>/dev/null || true + # Remove packages that depend on librtlsdr2, then remove librtlsdr2 + # These will be reinstalled with librtlsdr0 support + $SUDO apt-get remove -y dump1090-mutability libgnuradio-osmosdr0.2.0t64 rtl-433 librtlsdr2 rtl-sdr 2>/dev/null || true + $SUDO apt-get autoremove -y 2>/dev/null || true - ok "Removed conflicting librtlsdr2 packages" + ok "Removed conflicting librtlsdr2 packages" + fi + + # If rtl-sdr is in broken state, remove it completely first + if dpkg -l | grep -q "^.[^i].*rtl-sdr" || ! dpkg -l rtl-sdr 2>/dev/null | grep -q "^ii"; then + info "Removing broken rtl-sdr package..." + $SUDO dpkg --remove --force-remove-reinstreq rtl-sdr 2>/dev/null || true + $SUDO dpkg --purge --force-remove-reinstreq rtl-sdr 2>/dev/null || true + fi + + # Force remove librtlsdr2 if it still exists + if dpkg -l | grep -q "librtlsdr2"; then + info "Force removing librtlsdr2..." + $SUDO dpkg --remove --force-all librtlsdr2 2>/dev/null || true + $SUDO dpkg --purge --force-all librtlsdr2 2>/dev/null || true + fi + + # Clean up any partial installations + $SUDO dpkg --configure -a 2>/dev/null || true + $SUDO apt-get --fix-broken install -y 2>/dev/null || true fi - # If rtl-sdr is in broken state, remove it completely first - if dpkg -l | grep -q "^.[^i].*rtl-sdr" || ! dpkg -l rtl-sdr 2>/dev/null | grep -q "^ii"; then - info "Removing broken rtl-sdr package..." - $SUDO dpkg --remove --force-remove-reinstreq rtl-sdr 2>/dev/null || true - $SUDO dpkg --purge --force-remove-reinstreq rtl-sdr 2>/dev/null || true + apt_install_if_missing rtl-sdr + + progress "RTL-SDR Blog drivers" + if cmd_exists rtl_test; then + info "RTL-SDR tools already installed." + if $IS_DRAGONOS; then + info "Skipping RTL-SDR Blog driver installation (DragonOS has working drivers)." + else + echo "RTL-SDR Blog drivers provide improved support for V4 dongles." + echo "Installing these will REPLACE your current RTL-SDR drivers." + if ask_yes_no "Install RTL-SDR Blog drivers?"; then + install_rtlsdr_blog_drivers_debian + else + ok "Keeping existing RTL-SDR drivers." + fi + fi + else + install_rtlsdr_blog_drivers_debian fi - # Force remove librtlsdr2 if it still exists - if dpkg -l | grep -q "librtlsdr2"; then - info "Force removing librtlsdr2..." - $SUDO dpkg --remove --force-all librtlsdr2 2>/dev/null || true - $SUDO dpkg --purge --force-all librtlsdr2 2>/dev/null || true - fi - - # Clean up any partial installations - $SUDO dpkg --configure -a 2>/dev/null || true - $SUDO apt-get --fix-broken install -y 2>/dev/null || true - - apt_install rtl-sdr - - progress "Installing RTL-SDR Blog drivers (V4 support)" - install_rtlsdr_blog_drivers_debian - progress "Installing multimon-ng" apt_install multimon-ng @@ -650,8 +734,20 @@ install_debian_packages() { progress "Configuring udev rules" setup_udev_rules_debian - progress "Blacklisting conflicting kernel drivers" - blacklist_kernel_drivers_debian + progress "Kernel driver configuration" + echo + if $IS_DRAGONOS; then + info "DragonOS already has RTL-SDR drivers configured correctly." + info "Skipping kernel driver blacklist (not needed)." + else + echo "The DVB-T kernel drivers conflict with RTL-SDR userspace access." + echo "Blacklisting them allows rtl_sdr tools to access the device." + if ask_yes_no "Blacklist conflicting kernel drivers?"; then + blacklist_kernel_drivers_debian + else + warn "Skipped kernel driver blacklist. RTL-SDR may not work without manual config." + fi + fi } # ---------------------------- @@ -685,11 +781,42 @@ final_summary_and_hard_fail() { fi } +# ---------------------------- +# Pre-flight summary +# ---------------------------- +show_install_summary() { + info "Installation Summary:" + echo + echo " OS: $OS" + $IS_DRAGONOS && echo " DragonOS: Yes (safe mode enabled)" + echo + echo " This script will:" + echo " - Install missing SDR tools (rtl-sdr, multimon-ng, etc.)" + echo " - Install Python dependencies in a virtual environment" + echo + if ! $IS_DRAGONOS; then + echo " You will be prompted before:" + echo " - Installing RTL-SDR Blog drivers (replaces existing)" + echo " - Blacklisting kernel DVB drivers" + fi + echo + if $NON_INTERACTIVE; then + info "Non-interactive mode: continuing without prompt." + return + fi + if ! ask_yes_no "Continue with installation?" "y"; then + info "Installation cancelled." + exit 0 + fi +} + # ---------------------------- # MAIN # ---------------------------- main() { detect_os + detect_dragonos + show_install_summary if [[ "$OS" == "macos" ]]; then install_macos_packages @@ -702,4 +829,3 @@ main() { } main "$@" - diff --git a/templates/index.html b/templates/index.html index bf4ee39..f2df7b1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7860,6 +7860,7 @@ const wifiInterface = document.getElementById('tscmWifiInterface').value; const btInterface = document.getElementById('tscmBtInterface').value; const sdrDevice = document.getElementById('tscmSdrDevice').value; + const verboseResults = document.getElementById('tscmVerboseResults').checked; // Clear any previous warnings document.getElementById('tscmDeviceWarnings').style.display = 'none'; @@ -7877,7 +7878,8 @@ rf: rfEnabled, wifi_interface: wifiInterface, bt_interface: btInterface, - sdr_device: sdrDevice ? parseInt(sdrDevice) : null + sdr_device: sdrDevice ? parseInt(sdrDevice) : null, + verbose_results: verboseResults }) }); @@ -10562,4 +10564,4 @@ - \ No newline at end of file + diff --git a/templates/partials/modes/tscm.html b/templates/partials/modes/tscm.html index b212ae2..e59c5cf 100644 --- a/templates/partials/modes/tscm.html +++ b/templates/partials/modes/tscm.html @@ -23,6 +23,15 @@ +
+
+ + +
+
+
diff --git a/utils/tscm/reports.py b/utils/tscm/reports.py index 4c97bb9..27ba033 100644 --- a/utils/tscm/reports.py +++ b/utils/tscm/reports.py @@ -761,15 +761,27 @@ def generate_report( # Add findings from profiles builder.add_findings_from_profiles(device_profiles) - # Statistics - results = sweep_data.get('results', {}) - builder.add_statistics( - wifi=len(results.get('wifi', [])), - bluetooth=len(results.get('bluetooth', [])), - rf=len(results.get('rf', [])), - new=baseline_diff.get('summary', {}).get('new_devices', 0) if baseline_diff else 0, - missing=baseline_diff.get('summary', {}).get('missing_devices', 0) if baseline_diff else 0, - ) + # Statistics + results = sweep_data.get('results', {}) + wifi_count = results.get('wifi_count') + if wifi_count is None: + wifi_count = len(results.get('wifi_devices', results.get('wifi', []))) + + bt_count = results.get('bt_count') + if bt_count is None: + bt_count = len(results.get('bt_devices', results.get('bluetooth', []))) + + rf_count = results.get('rf_count') + if rf_count is None: + rf_count = len(results.get('rf_signals', results.get('rf', []))) + + builder.add_statistics( + wifi=wifi_count, + bluetooth=bt_count, + rf=rf_count, + new=baseline_diff.get('summary', {}).get('new_devices', 0) if baseline_diff else 0, + missing=baseline_diff.get('summary', {}).get('missing_devices', 0) if baseline_diff else 0, + ) # Technical data builder.add_device_timelines(timelines)