mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 09:08:12 -07:00
96172ca593
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>
253 lines
8.3 KiB
Python
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
|