""" TSCM Analysis Routes Handles /threats/*, /report/*, /wifi/*, /bluetooth/*, /playbooks/*, /findings/*, /identity/*, /known-devices/*, /device/*/timeline, and /timelines endpoints. """ from __future__ import annotations import logging from datetime import datetime from flask import Response, jsonify, request from routes.tscm import ( _generate_assessment, tscm_bp, ) from utils.database import ( acknowledge_tscm_threat, get_active_tscm_baseline, get_tscm_sweep, get_tscm_threat_summary, get_tscm_threats, ) from utils.tscm.correlation import get_correlation_engine logger = logging.getLogger('intercept.tscm') # ============================================================================= # Threat Endpoints # ============================================================================= @tscm_bp.route('/threats') def list_threats(): """List threats with optional filters.""" sweep_id = request.args.get('sweep_id', type=int) severity = request.args.get('severity') acknowledged = request.args.get('acknowledged') limit = request.args.get('limit', 100, type=int) ack_filter = None if acknowledged is not None: ack_filter = acknowledged.lower() in ('true', '1', 'yes') threats = get_tscm_threats( sweep_id=sweep_id, severity=severity, acknowledged=ack_filter, limit=limit ) return jsonify({'status': 'success', 'threats': threats}) @tscm_bp.route('/threats/summary') def threat_summary(): """Get threat count summary by severity.""" summary = get_tscm_threat_summary() return jsonify({'status': 'success', 'summary': summary}) @tscm_bp.route('/threats/', methods=['PUT']) def update_threat(threat_id: int): """Update a threat (acknowledge, add notes).""" data = request.get_json() or {} if data.get('acknowledge'): notes = data.get('notes') success = acknowledge_tscm_threat(threat_id, notes) if not success: return jsonify({'status': 'error', 'message': 'Threat not found'}), 404 return jsonify({'status': 'success', 'message': 'Threat updated'}) # ============================================================================= # Correlation & Findings Endpoints # ============================================================================= @tscm_bp.route('/findings') def get_findings(): """ Get comprehensive TSCM findings from the correlation engine. Returns all device profiles organized by risk level, cross-protocol correlations, and summary statistics with client-safe disclaimers. """ correlation = get_correlation_engine() findings = correlation.get_all_findings() # Add client-safe disclaimer findings['legal_disclaimer'] = ( "DISCLAIMER: This TSCM screening system identifies wireless and RF anomalies " "and indicators. Results represent potential items of interest, NOT confirmed " "surveillance devices. No content has been intercepted or decoded. Findings " "require professional analysis and verification. This tool does not prove " "malicious intent or illegal activity." ) return jsonify({ 'status': 'success', 'findings': findings }) @tscm_bp.route('/findings/high-interest') def get_high_interest(): """Get only high-interest devices (score >= 6).""" correlation = get_correlation_engine() high_interest = correlation.get_high_interest_devices() return jsonify({ 'status': 'success', 'count': len(high_interest), 'devices': [d.to_dict() for d in high_interest], 'disclaimer': ( "High-interest classification indicates multiple indicators warrant " "investigation. This does NOT confirm surveillance activity." ) }) @tscm_bp.route('/findings/correlations') def get_correlations(): """Get cross-protocol correlation analysis.""" correlation = get_correlation_engine() correlations = correlation.correlate_devices() return jsonify({ 'status': 'success', 'count': len(correlations), 'correlations': correlations, 'explanation': ( "Correlations identify devices across different protocols (Bluetooth, " "WiFi, RF) that exhibit related behavior patterns. Cross-protocol " "activity is one indicator among many in TSCM analysis." ) }) @tscm_bp.route('/findings/device/') def get_device_profile(identifier: str): """Get detailed profile for a specific device.""" correlation = get_correlation_engine() # Search all protocols for the identifier for protocol in ['bluetooth', 'wifi', 'rf']: key = f"{protocol}:{identifier}" if key in correlation.device_profiles: profile = correlation.device_profiles[key] return jsonify({ 'status': 'success', 'profile': profile.to_dict() }) return jsonify({ 'status': 'error', 'message': 'Device not found' }), 404 # ============================================================================= # Report Generation Endpoints # ============================================================================= @tscm_bp.route('/report') def generate_report(): """ Generate a comprehensive TSCM sweep report. Includes all findings, correlations, indicators, and recommended actions in a client-presentable format with appropriate disclaimers. """ correlation = get_correlation_engine() findings = correlation.get_all_findings() # Build the report structure report = { 'generated_at': datetime.now().isoformat(), 'report_type': 'TSCM Wireless Surveillance Screening', 'executive_summary': { 'total_devices_analyzed': findings['summary']['total_devices'], 'high_interest_items': findings['summary']['high_interest'], 'items_requiring_review': findings['summary']['needs_review'], 'cross_protocol_correlations': findings['summary']['correlations_found'], 'assessment': _generate_assessment(findings['summary']), }, 'methodology': { 'protocols_scanned': ['Bluetooth Low Energy', 'WiFi 802.11', 'RF Spectrum'], 'analysis_techniques': [ 'Device fingerprinting', 'Signal stability analysis', 'Cross-protocol correlation', 'Time-based pattern detection', 'Manufacturer identification', ], 'scoring_model': { 'informational': '0-2 points - Known or expected devices', 'needs_review': '3-5 points - Unusual devices requiring assessment', 'high_interest': '6+ points - Multiple indicators warrant investigation', } }, 'findings': { 'high_interest': findings['devices']['high_interest'], 'needs_review': findings['devices']['needs_review'], 'informational': findings['devices']['informational'], }, 'correlations': findings['correlations'], 'disclaimers': { 'legal': ( "This report documents findings from a wireless and RF surveillance " "screening. Results indicate anomalies and items of interest, NOT " "confirmed surveillance devices. No communications content has been " "intercepted, recorded, or decoded. This screening does not prove " "malicious intent, illegal activity, or the presence of surveillance " "equipment. All findings require professional verification." ), 'technical': ( "Detection capabilities are limited by equipment sensitivity, " "environmental factors, and the technical sophistication of any " "potential devices. Absence of findings does NOT guarantee absence " "of surveillance equipment." ), 'recommendations': ( "High-interest items should be investigated by qualified TSCM " "professionals using appropriate physical inspection techniques. " "This electronic sweep is one component of comprehensive TSCM." ) } } return jsonify({ 'status': 'success', 'report': report }) @tscm_bp.route('/report/pdf') def get_pdf_report(): """ Generate client-safe PDF report. Contains executive summary, findings by risk tier, meeting window summary, and mandatory disclaimers. """ try: from routes.tscm import _current_sweep_id from utils.tscm.advanced import detect_sweep_capabilities, get_timeline_manager from utils.tscm.reports import generate_report, get_pdf_report sweep_id = request.args.get('sweep_id', _current_sweep_id, type=int) if not sweep_id: return jsonify({'status': 'error', 'message': 'No sweep specified'}), 400 sweep = get_tscm_sweep(sweep_id) if not sweep: return jsonify({'status': 'error', 'message': 'Sweep not found'}), 404 # Get data for report correlation = get_correlation_engine() profiles = [p.to_dict() for p in correlation.device_profiles.values()] caps = detect_sweep_capabilities().to_dict() manager = get_timeline_manager() timelines = [t.to_dict() for t in manager.get_all_timelines()] # Generate report report = generate_report( sweep_id=sweep_id, sweep_data=sweep, device_profiles=profiles, capabilities=caps, timelines=timelines ) pdf_content = get_pdf_report(report) return Response( pdf_content, mimetype='text/plain', headers={ 'Content-Disposition': f'attachment; filename=tscm_report_{sweep_id}.txt' } ) except Exception as e: logger.error(f"Generate PDF report error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/report/annex') def get_technical_annex(): """ Generate technical annex (JSON + CSV). Contains device timelines, all indicators, and detailed data for audit purposes. No packet data included. """ try: from routes.tscm import _current_sweep_id from utils.tscm.advanced import detect_sweep_capabilities, get_timeline_manager from utils.tscm.reports import generate_report, get_csv_annex, get_json_annex sweep_id = request.args.get('sweep_id', _current_sweep_id, type=int) format_type = request.args.get('format', 'json') if not sweep_id: return jsonify({'status': 'error', 'message': 'No sweep specified'}), 400 sweep = get_tscm_sweep(sweep_id) if not sweep: return jsonify({'status': 'error', 'message': 'Sweep not found'}), 404 # Get data for report correlation = get_correlation_engine() profiles = [p.to_dict() for p in correlation.device_profiles.values()] caps = detect_sweep_capabilities().to_dict() manager = get_timeline_manager() timelines = [t.to_dict() for t in manager.get_all_timelines()] # Generate report report = generate_report( sweep_id=sweep_id, sweep_data=sweep, device_profiles=profiles, capabilities=caps, timelines=timelines ) if format_type == 'csv': csv_content = get_csv_annex(report) return Response( csv_content, mimetype='text/csv', headers={ 'Content-Disposition': f'attachment; filename=tscm_annex_{sweep_id}.csv' } ) else: annex = get_json_annex(report) return jsonify({ 'status': 'success', 'annex': annex }) except Exception as e: logger.error(f"Generate technical annex error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 # ============================================================================= # WiFi Advanced Indicators Endpoints # ============================================================================= @tscm_bp.route('/wifi/advanced-indicators') def get_wifi_advanced_indicators(): """ Get advanced WiFi indicators (Evil Twin, Probes, Deauth). These indicators require analysis of WiFi patterns. Some features require monitor mode. """ try: from utils.tscm.advanced import get_wifi_detector detector = get_wifi_detector() return jsonify({ 'status': 'success', 'indicators': detector.get_all_indicators(), 'unavailable_features': detector.get_unavailable_features(), 'disclaimer': ( "All indicators represent pattern detections, NOT confirmed attacks. " "Further investigation is required." ) }) except Exception as e: logger.error(f"Get WiFi indicators error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/wifi/analyze-network', methods=['POST']) def analyze_wifi_network(): """ Analyze a WiFi network for evil twin patterns. Compares against known networks to detect SSID spoofing. """ try: from utils.tscm.advanced import get_wifi_detector data = request.get_json() or {} detector = get_wifi_detector() # Set known networks from baseline if available baseline = get_active_tscm_baseline() if baseline: detector.set_known_networks(baseline.get('wifi_networks', [])) indicators = detector.analyze_network(data) return jsonify({ 'status': 'success', 'indicators': [i.to_dict() for i in indicators] }) except Exception as e: logger.error(f"Analyze WiFi network error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 # ============================================================================= # Bluetooth Risk Explainability Endpoints # ============================================================================= @tscm_bp.route('/bluetooth//explain') def explain_bluetooth_risk(identifier: str): """ Get human-readable risk explanation for a BLE device. Includes proximity estimate, tracker explanation, and recommended actions. """ try: from utils.tscm.advanced import generate_ble_risk_explanation # Get device from correlation engine correlation = get_correlation_engine() profile = None key = f"bluetooth:{identifier.upper()}" if key in correlation.device_profiles: profile = correlation.device_profiles[key].to_dict() # Try to find device info device = {'mac': identifier} if profile: device['name'] = profile.get('name') device['rssi'] = profile.get('rssi_samples', [None])[-1] if profile.get('rssi_samples') else None # Check meeting status is_meeting = correlation.is_during_meeting() explanation = generate_ble_risk_explanation(device, profile, is_meeting) return jsonify({ 'status': 'success', 'explanation': explanation.to_dict() }) except Exception as e: logger.error(f"Explain BLE risk error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/bluetooth//proximity') def get_bluetooth_proximity(identifier: str): """Get proximity estimate for a BLE device.""" try: from utils.tscm.advanced import estimate_ble_proximity rssi = request.args.get('rssi', type=int) if rssi is None: # Try to get from correlation engine correlation = get_correlation_engine() key = f"bluetooth:{identifier.upper()}" if key in correlation.device_profiles: profile = correlation.device_profiles[key] if profile.rssi_samples: rssi = profile.rssi_samples[-1] if rssi is None: return jsonify({ 'status': 'error', 'message': 'RSSI value required' }), 400 proximity, explanation, distance = estimate_ble_proximity(rssi) return jsonify({ 'status': 'success', 'proximity': { 'estimate': proximity.value, 'explanation': explanation, 'estimated_distance': distance, 'rssi_used': rssi, }, 'disclaimer': ( "Proximity estimates are approximate and affected by " "environment, obstacles, and device characteristics." ) }) except Exception as e: logger.error(f"Get BLE proximity error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 # ============================================================================= # Operator Playbook Endpoints # ============================================================================= @tscm_bp.route('/playbooks') def list_playbooks(): """List all available operator playbooks.""" try: from utils.tscm.advanced import PLAYBOOKS # Return as array with id field for JavaScript compatibility playbooks_list = [] for pid, pb in PLAYBOOKS.items(): pb_dict = pb.to_dict() pb_dict['id'] = pid pb_dict['name'] = pb_dict.get('title', pid) pb_dict['category'] = pb_dict.get('risk_level', 'general') playbooks_list.append(pb_dict) return jsonify({ 'status': 'success', 'playbooks': playbooks_list }) except Exception as e: logger.error(f"List playbooks error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/playbooks/') def get_playbook(playbook_id: str): """Get a specific playbook.""" try: from utils.tscm.advanced import PLAYBOOKS if playbook_id not in PLAYBOOKS: return jsonify({'status': 'error', 'message': 'Playbook not found'}), 404 return jsonify({ 'status': 'success', 'playbook': PLAYBOOKS[playbook_id].to_dict() }) except Exception as e: logger.error(f"Get playbook error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/findings//playbook') def get_finding_playbook(identifier: str): """Get recommended playbook for a specific finding.""" try: from utils.tscm.advanced import get_playbook_for_finding # Get profile correlation = get_correlation_engine() profile = None for protocol in ['bluetooth', 'wifi', 'rf']: key = f"{protocol}:{identifier.upper()}" if key in correlation.device_profiles: profile = correlation.device_profiles[key].to_dict() break if not profile: return jsonify({'status': 'error', 'message': 'Finding not found'}), 404 playbook = get_playbook_for_finding( risk_level=profile.get('risk_level', 'informational'), indicators=profile.get('indicators', []) ) return jsonify({ 'status': 'success', 'playbook': playbook.to_dict(), 'suggested_next_steps': [ f"Step {s.step_number}: {s.action}" for s in playbook.steps[:3] ] }) except Exception as e: logger.error(f"Get finding playbook error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 # ============================================================================= # Device Identity Endpoints (MAC-Randomization Resistant Detection) # ============================================================================= @tscm_bp.route('/identity/ingest/ble', methods=['POST']) def ingest_ble_observation(): """ Ingest a BLE observation for device identity clustering. This endpoint accepts BLE advertisement data and feeds it into the MAC-randomization resistant device detection engine. Expected JSON payload: { "timestamp": "2024-01-01T12:00:00", // ISO format or omit for now "addr": "AA:BB:CC:DD:EE:FF", // BLE address (may be randomized) "addr_type": "rpa", // public/random_static/rpa/nrpa/unknown "rssi": -65, // dBm "tx_power": -10, // dBm (optional) "adv_type": "ADV_IND", // Advertisement type "manufacturer_id": 1234, // Company ID (optional) "manufacturer_data": "0102030405", // Hex string (optional) "service_uuids": ["uuid1", "uuid2"], // List of UUIDs (optional) "local_name": "Device Name", // Advertised name (optional) "appearance": 960, // BLE appearance (optional) "packet_length": 31 // Total packet length (optional) } """ try: from utils.tscm.device_identity import ingest_ble_dict data = request.get_json() if not data: return jsonify({'status': 'error', 'message': 'No data provided'}), 400 session = ingest_ble_dict(data) return jsonify({ 'status': 'success', 'session_id': session.session_id, 'observation_count': len(session.observations), }) except Exception as e: logger.error(f"BLE ingestion error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/identity/ingest/wifi', methods=['POST']) def ingest_wifi_observation(): """ Ingest a WiFi observation for device identity clustering. Expected JSON payload: { "timestamp": "2024-01-01T12:00:00", "src_mac": "AA:BB:CC:DD:EE:FF", // Client MAC (may be randomized) "dst_mac": "11:22:33:44:55:66", // Destination MAC "bssid": "11:22:33:44:55:66", // AP BSSID "ssid": "NetworkName", // SSID if available "frame_type": "probe_request", // Frame type "rssi": -70, // dBm "channel": 6, // WiFi channel "ht_capable": true, // 802.11n capable "vht_capable": true, // 802.11ac capable "he_capable": false, // 802.11ax capable "supported_rates": [1, 2, 5.5, 11], // Supported rates "vendor_ies": [["001122", 10]], // [(OUI, length), ...] "probed_ssids": ["ssid1", "ssid2"] // For probe requests } """ try: from utils.tscm.device_identity import ingest_wifi_dict data = request.get_json() if not data: return jsonify({'status': 'error', 'message': 'No data provided'}), 400 session = ingest_wifi_dict(data) return jsonify({ 'status': 'success', 'session_id': session.session_id, 'observation_count': len(session.observations), }) except Exception as e: logger.error(f"WiFi ingestion error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/identity/ingest/batch', methods=['POST']) def ingest_batch_observations(): """ Ingest multiple observations in a single request. Expected JSON payload: { "ble": [, ...], "wifi": [, ...] } """ try: from utils.tscm.device_identity import ingest_ble_dict, ingest_wifi_dict data = request.get_json() if not data: return jsonify({'status': 'error', 'message': 'No data provided'}), 400 ble_count = 0 wifi_count = 0 for ble_obs in data.get('ble', []): ingest_ble_dict(ble_obs) ble_count += 1 for wifi_obs in data.get('wifi', []): ingest_wifi_dict(wifi_obs) wifi_count += 1 return jsonify({ 'status': 'success', 'ble_ingested': ble_count, 'wifi_ingested': wifi_count, }) except Exception as e: logger.error(f"Batch ingestion error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/identity/clusters') def get_device_clusters(): """ Get all device clusters (probable physical device identities). Query parameters: - min_confidence: Minimum cluster confidence (0-1, default 0) - protocol: Filter by protocol ('ble' or 'wifi') - risk_level: Filter by risk level ('high', 'medium', 'low', 'informational') """ try: from utils.tscm.device_identity import get_identity_engine engine = get_identity_engine() min_conf = request.args.get('min_confidence', 0, type=float) protocol = request.args.get('protocol') risk_filter = request.args.get('risk_level') clusters = engine.get_clusters(min_confidence=min_conf) if protocol: clusters = [c for c in clusters if c.protocol == protocol] if risk_filter: clusters = [c for c in clusters if c.risk_level.value == risk_filter] return jsonify({ 'status': 'success', 'count': len(clusters), 'clusters': [c.to_dict() for c in clusters], 'disclaimer': ( "Clusters represent PROBABLE device identities based on passive " "fingerprinting. Results are statistical correlations, not " "confirmed matches. False positives/negatives are expected." ) }) except Exception as e: logger.error(f"Get clusters error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/identity/clusters/high-risk') def get_high_risk_clusters(): """Get device clusters with HIGH risk level.""" try: from utils.tscm.device_identity import get_identity_engine engine = get_identity_engine() clusters = engine.get_high_risk_clusters() return jsonify({ 'status': 'success', 'count': len(clusters), 'clusters': [c.to_dict() for c in clusters], 'disclaimer': ( "High-risk classification indicates multiple behavioral indicators " "consistent with potential surveillance devices. This does NOT " "confirm surveillance activity. Professional verification required." ) }) except Exception as e: logger.error(f"Get high-risk clusters error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/identity/summary') def get_identity_summary(): """ Get summary of device identity analysis. Returns statistics, cluster counts by risk level, and monitoring period. """ try: from utils.tscm.device_identity import get_identity_engine engine = get_identity_engine() summary = engine.get_summary() return jsonify({ 'status': 'success', 'summary': summary }) except Exception as e: logger.error(f"Get identity summary error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/identity/finalize', methods=['POST']) def finalize_identity_sessions(): """ Finalize all active sessions and complete clustering. Call this at the end of a monitoring period to ensure all observations are properly clustered and assessed. """ try: from utils.tscm.device_identity import get_identity_engine engine = get_identity_engine() engine.finalize_all_sessions() summary = engine.get_summary() return jsonify({ 'status': 'success', 'message': 'All sessions finalized', 'summary': summary }) except Exception as e: logger.error(f"Finalize sessions error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/identity/reset', methods=['POST']) def reset_identity_engine(): """ Reset the device identity engine. Clears all sessions, clusters, and monitoring state. """ try: from utils.tscm.device_identity import reset_identity_engine as reset_engine reset_engine() return jsonify({ 'status': 'success', 'message': 'Device identity engine reset' }) except Exception as e: logger.error(f"Reset identity engine error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/identity/cluster/') def get_cluster_detail(cluster_id: str): """Get detailed information for a specific cluster.""" try: from utils.tscm.device_identity import get_identity_engine engine = get_identity_engine() if cluster_id not in engine.clusters: return jsonify({ 'status': 'error', 'message': 'Cluster not found' }), 404 cluster = engine.clusters[cluster_id] return jsonify({ 'status': 'success', 'cluster': cluster.to_dict() }) except Exception as e: logger.error(f"Get cluster detail error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 # ============================================================================= # Device Timeline Endpoints # ============================================================================= @tscm_bp.route('/device//timeline') def get_device_timeline_endpoint(identifier: str): """ Get timeline of observations for a device. Shows behavior over time including RSSI stability, presence, and meeting window correlation. """ try: from utils.database import get_device_timeline from utils.tscm.advanced import get_timeline_manager protocol = request.args.get('protocol', 'bluetooth') since_hours = request.args.get('since_hours', 24, type=int) # Try in-memory timeline first manager = get_timeline_manager() timeline = manager.get_timeline(identifier, protocol) # Also get stored timeline from database stored = get_device_timeline(identifier, since_hours=since_hours) result = { 'identifier': identifier, 'protocol': protocol, 'observations': stored, } if timeline: result['metrics'] = { 'first_seen': timeline.first_seen.isoformat() if timeline.first_seen else None, 'last_seen': timeline.last_seen.isoformat() if timeline.last_seen else None, 'total_observations': timeline.total_observations, 'presence_ratio': round(timeline.presence_ratio, 2), } result['signal'] = { 'rssi_min': timeline.rssi_min, 'rssi_max': timeline.rssi_max, 'rssi_mean': round(timeline.rssi_mean, 1) if timeline.rssi_mean else None, 'stability': round(timeline.rssi_stability, 2), } result['movement'] = { 'appears_stationary': timeline.appears_stationary, 'pattern': timeline.movement_pattern, } result['meeting_correlation'] = { 'correlated': timeline.meeting_correlated, 'observations_during_meeting': timeline.meeting_observations, } return jsonify({ 'status': 'success', 'timeline': result }) except Exception as e: logger.error(f"Get device timeline error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @tscm_bp.route('/timelines') def get_all_device_timelines(): """Get all device timelines.""" try: from utils.tscm.advanced import get_timeline_manager manager = get_timeline_manager() timelines = manager.get_all_timelines() return jsonify({ 'status': 'success', 'count': len(timelines), 'timelines': [t.to_dict() for t in timelines] }) except Exception as e: logger.error(f"Get all timelines error: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 # ============================================================================= # Known-Good Registry (Whitelist) Endpoints # ============================================================================= @tscm_bp.route('/known-devices', methods=['GET']) def list_known_devices(): """List all known-good devices.""" from utils.database import get_all_known_devices location = request.args.get('location') scope = request.args.get('scope') devices = get_all_known_devices(location=location, scope=scope) return jsonify({ 'status': 'success', 'count': len(devices), 'devices': devices }) @tscm_bp.route('/known-devices', methods=['POST']) def add_known_device_endpoint(): """ Add a device to the known-good registry. Known devices remain visible but receive reduced risk scores. They are NOT suppressed from reports (preserves audit trail). """ from utils.database import add_known_device data = request.get_json() or {} identifier = data.get('identifier') protocol = data.get('protocol') if not identifier or not protocol: return jsonify({ 'status': 'error', 'message': 'identifier and protocol are required' }), 400 device_id = add_known_device( identifier=identifier, protocol=protocol, name=data.get('name'), description=data.get('description'), location=data.get('location'), scope=data.get('scope', 'global'), added_by=data.get('added_by'), score_modifier=data.get('score_modifier', -2), metadata=data.get('metadata') ) return jsonify({ 'status': 'success', 'message': 'Device added to known-good registry', 'device_id': device_id }) @tscm_bp.route('/known-devices/', methods=['GET']) def get_known_device_endpoint(identifier: str): """Get a known device by identifier.""" from utils.database import get_known_device device = get_known_device(identifier) if not device: return jsonify({'status': 'error', 'message': 'Device not found'}), 404 return jsonify({ 'status': 'success', 'device': device }) @tscm_bp.route('/known-devices/', methods=['DELETE']) def delete_known_device_endpoint(identifier: str): """Remove a device from the known-good registry.""" from utils.database import delete_known_device success = delete_known_device(identifier) if not success: return jsonify({'status': 'error', 'message': 'Device not found'}), 404 return jsonify({ 'status': 'success', 'message': 'Device removed from known-good registry' }) @tscm_bp.route('/known-devices/check/') def check_known_device(identifier: str): """Check if a device is in the known-good registry.""" from utils.database import is_known_good_device location = request.args.get('location') result = is_known_good_device(identifier, location=location) return jsonify({ 'status': 'success', 'is_known': result is not None, 'details': result })