Files
intercept/routes/tscm/baseline.py
T
James Smith 96172ca593 style: apply ruff-format to entire codebase
First-time run of ruff-format via pre-commit hook normalises quote
style, trailing commas, and whitespace across 188 Python files.
No logic changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 14:48:11 +01:00

253 lines
8.3 KiB
Python

"""
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/<int:baseline_id>")
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/<int:baseline_id>/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/<int:baseline_id>", 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/<int:baseline_id>/<int:sweep_id>")
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/<int:baseline_id>/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