""" TSCM Baseline Routes Handles /baseline/*, /baselines endpoints. """ from __future__ import annotations import json import logging from datetime import datetime from flask import jsonify, request from routes.tscm import ( _baseline_recorder, tscm_bp, ) from utils.database import ( delete_tscm_baseline, get_active_tscm_baseline, get_all_tscm_baselines, get_tscm_baseline, get_tscm_sweep, set_active_tscm_baseline, ) from utils.tscm.baseline import ( get_comparison_for_active_baseline, ) logger = logging.getLogger("intercept.tscm") @tscm_bp.route("/baseline/record", methods=["POST"]) def record_baseline(): """Start recording a new baseline.""" data = request.get_json() or {} name = data.get("name", f"Baseline {datetime.now().strftime('%Y-%m-%d %H:%M')}") location = data.get("location") description = data.get("description") baseline_id = _baseline_recorder.start_recording(name, location, description) return jsonify({"status": "success", "message": "Baseline recording started", "baseline_id": baseline_id}) @tscm_bp.route("/baseline/stop", methods=["POST"]) def stop_baseline(): """Stop baseline recording.""" result = _baseline_recorder.stop_recording() if "error" in result: return jsonify({"status": "error", "message": result["error"]}) return jsonify({"status": "success", "message": "Baseline recording complete", **result}) @tscm_bp.route("/baseline/status") def baseline_status(): """Get baseline recording status.""" return jsonify(_baseline_recorder.get_recording_status()) @tscm_bp.route("/baselines") def list_baselines(): """List all baselines.""" baselines = get_all_tscm_baselines() return jsonify({"status": "success", "baselines": baselines}) @tscm_bp.route("/baseline/") def get_baseline(baseline_id: int): """Get a specific baseline.""" baseline = get_tscm_baseline(baseline_id) if not baseline: return jsonify({"status": "error", "message": "Baseline not found"}), 404 return jsonify({"status": "success", "baseline": baseline}) @tscm_bp.route("/baseline//activate", methods=["POST"]) def activate_baseline(baseline_id: int): """Set a baseline as active.""" success = set_active_tscm_baseline(baseline_id) if not success: return jsonify({"status": "error", "message": "Baseline not found"}), 404 return jsonify({"status": "success", "message": "Baseline activated"}) @tscm_bp.route("/baseline/", methods=["DELETE"]) def remove_baseline(baseline_id: int): """Delete a baseline.""" success = delete_tscm_baseline(baseline_id) if not success: return jsonify({"status": "error", "message": "Baseline not found"}), 404 return jsonify({"status": "success", "message": "Baseline deleted"}) @tscm_bp.route("/baseline/active") def get_active_baseline(): """Get the currently active baseline.""" baseline = get_active_tscm_baseline() if not baseline: return jsonify({"status": "success", "baseline": None}) return jsonify({"status": "success", "baseline": baseline}) @tscm_bp.route("/baseline/compare", methods=["POST"]) def compare_against_baseline(): """ Compare provided device data against the active baseline. Expects JSON body with: - wifi_devices: list of WiFi devices (optional) - wifi_clients: list of WiFi clients (optional) - bt_devices: list of Bluetooth devices (optional) - rf_signals: list of RF signals (optional) Returns comparison showing new, missing, and matching devices. """ data = request.get_json() or {} wifi_devices = data.get("wifi_devices") wifi_clients = data.get("wifi_clients") bt_devices = data.get("bt_devices") rf_signals = data.get("rf_signals") # Use the convenience function that gets active baseline comparison = get_comparison_for_active_baseline( wifi_devices=wifi_devices, wifi_clients=wifi_clients, bt_devices=bt_devices, rf_signals=rf_signals ) if comparison is None: return jsonify({"status": "error", "message": "No active baseline set"}), 400 return jsonify({"status": "success", "comparison": comparison}) # ============================================================================= # Baseline Diff & Health Endpoints # ============================================================================= @tscm_bp.route("/baseline/diff//") def get_baseline_diff(baseline_id: int, sweep_id: int): """ Get comprehensive diff between a baseline and a sweep. Shows new devices, missing devices, changed characteristics, and baseline health assessment. """ try: from utils.tscm.advanced import calculate_baseline_diff baseline = get_tscm_baseline(baseline_id) if not baseline: return jsonify({"status": "error", "message": "Baseline not found"}), 404 sweep = get_tscm_sweep(sweep_id) if not sweep: return jsonify({"status": "error", "message": "Sweep not found"}), 404 # Get current devices from sweep results results = sweep.get("results", {}) if isinstance(results, str): results = json.loads(results) current_wifi = results.get("wifi_devices", []) current_wifi_clients = results.get("wifi_clients", []) current_bt = results.get("bt_devices", []) current_rf = results.get("rf_signals", []) diff = calculate_baseline_diff( baseline=baseline, current_wifi=current_wifi, current_wifi_clients=current_wifi_clients, current_bt=current_bt, current_rf=current_rf, sweep_id=sweep_id, ) return jsonify({"status": "success", "diff": diff.to_dict()}) except Exception as e: logger.error(f"Get baseline diff error: {e}") return jsonify({"status": "error", "message": str(e)}), 500 @tscm_bp.route("/baseline//health") def get_baseline_health(baseline_id: int): """Get health assessment for a baseline.""" try: baseline = get_tscm_baseline(baseline_id) if not baseline: return jsonify({"status": "error", "message": "Baseline not found"}), 404 # Calculate age created_at = baseline.get("created_at") age_hours = 0 if created_at: if isinstance(created_at, str): created = datetime.fromisoformat(created_at.replace("Z", "+00:00")) age_hours = (datetime.now() - created.replace(tzinfo=None)).total_seconds() / 3600 elif isinstance(created_at, datetime): age_hours = (datetime.now() - created_at).total_seconds() / 3600 # Count devices total_devices = ( len(baseline.get("wifi_networks", [])) + len(baseline.get("bt_devices", [])) + len(baseline.get("rf_frequencies", [])) ) # Determine health health = "healthy" score = 1.0 reasons = [] if age_hours > 168: health = "stale" score = 0.3 reasons.append(f"Baseline is {age_hours:.0f} hours old (over 1 week)") elif age_hours > 72: health = "noisy" score = 0.6 reasons.append(f"Baseline is {age_hours:.0f} hours old (over 3 days)") if total_devices < 3: score -= 0.2 reasons.append(f"Baseline has few devices ({total_devices})") if health == "healthy": health = "noisy" return jsonify( { "status": "success", "health": { "status": health, "score": round(max(0, score), 2), "age_hours": round(age_hours, 1), "total_devices": total_devices, "reasons": reasons, }, } ) except Exception as e: logger.error(f"Get baseline health error: {e}") return jsonify({"status": "error", "message": str(e)}), 500