/* * Bluehound - Continuous BLE/Classic Bluetooth scanner with time-searchable logging * Built for embedded devices with WiFi streaming support */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // HTTP server library - using cpp-httplib (single header) #include "httplib.h" // Classic Bluetooth support #include "classic_bluetooth.h" // Configuration struct Config { int scan_duration_ms = 5000; // Scan for 5 seconds per cycle int scan_interval_ms = 1000; // Wait 1 second between scans std::string db_path = "ble_scans.db"; int http_port = 8080; bool enable_http_server = true; int max_db_entries = 1000000; // Rotate after 1M entries }; // Global state std::atomic running(true); std::mutex db_mutex; std::set enumerated_devices; // Track devices we've already enumerated std::mutex enumerated_mutex; Config config; // Forward declarations std::string get_timestamp(); // Signal handler for graceful shutdown void signal_handler(int signal) { std::cout << "\nReceived signal " << signal << ", shutting down gracefully..." << std::endl; running = false; } // BLE UUID Decoder - Standard Services std::string decode_service_uuid(const std::string& uuid) { static const std::map services = { {"1800", "Generic Access"}, {"1801", "Generic Attribute"}, {"1804", "Tx Power"}, {"180A", "Device Information"}, {"180D", "Heart Rate"}, {"180F", "Battery Service"}, {"1810", "Blood Pressure"}, {"1812", "Human Interface Device"}, {"1816", "Cycling Speed and Cadence"}, {"1818", "Cycling Power"}, {"181A", "Environmental Sensing"}, {"181B", "Body Composition"}, {"181C", "User Data"}, {"181D", "Weight Scale"} }; std::string upper_uuid = uuid; std::transform(upper_uuid.begin(), upper_uuid.end(), upper_uuid.begin(), ::toupper); // Extract 16-bit UUID from 128-bit format (0000XXXX-0000-1000-8000-00805f9b34fb) if (upper_uuid.length() >= 8 && upper_uuid.substr(0, 4) == "0000") { upper_uuid = upper_uuid.substr(4, 4); } else if (upper_uuid.length() > 4) { return ""; // Custom vendor UUID } auto it = services.find(upper_uuid); return (it != services.end()) ? it->second : ""; } // BLE UUID Decoder - Standard Characteristics std::string decode_characteristic_uuid(const std::string& uuid) { static const std::map characteristics = { {"2A00", "Device Name"}, {"2A01", "Appearance"}, {"2A04", "Peripheral Preferred Connection Parameters"}, {"2A05", "Service Changed"}, {"2A07", "Tx Power Level"}, {"2A19", "Battery Level"}, {"2A23", "System ID"}, {"2A24", "Model Number String"}, {"2A25", "Serial Number String"}, {"2A26", "Firmware Revision String"}, {"2A27", "Hardware Revision String"}, {"2A28", "Software Revision String"}, {"2A29", "Manufacturer Name String"}, {"2A37", "Heart Rate Measurement"}, {"2A38", "Body Sensor Location"}, {"2A49", "Blood Pressure Feature"}, {"2A4A", "HID Information"}, {"2A50", "PnP ID"}, {"2A6E", "Temperature"}, {"2A6F", "Humidity"}, {"2A9E", "Weight Measurement"} }; std::string upper_uuid = uuid; std::transform(upper_uuid.begin(), upper_uuid.end(), upper_uuid.begin(), ::toupper); // Extract 16-bit UUID from 128-bit format (0000XXXX-0000-1000-8000-00805f9b34fb) if (upper_uuid.length() >= 8 && upper_uuid.substr(0, 4) == "0000") { upper_uuid = upper_uuid.substr(4, 4); } else if (upper_uuid.length() > 4) { return ""; // Custom vendor UUID } auto it = characteristics.find(upper_uuid); return (it != characteristics.end()) ? it->second : ""; } // Get characteristic description std::string get_characteristic_description(const std::string& uuid) { static const std::map descriptions = { {"2A19", "Battery charge level (0-100%)"}, {"2A00", "Human-readable device name"}, {"2A29", "Name of device manufacturer"}, {"2A24", "Device model number"}, {"2A26", "Firmware version"}, {"2A37", "Heart rate in beats per minute"}, {"2A6E", "Temperature in Celsius"}, {"2A6F", "Relative humidity percentage"}, {"2A50", "Product ID information"}, {"2A07", "Transmit power level"} }; std::string upper_uuid = uuid; std::transform(upper_uuid.begin(), upper_uuid.end(), upper_uuid.begin(), ::toupper); // Extract 16-bit UUID from 128-bit format if (upper_uuid.length() >= 8 && upper_uuid.substr(0, 4) == "0000") { upper_uuid = upper_uuid.substr(4, 4); } else if (upper_uuid.length() > 4) { return ""; } auto it = descriptions.find(upper_uuid); return (it != descriptions.end()) ? it->second : ""; } // Security Assessment Structure struct Vulnerability { std::string name; std::string severity; // HIGH, MEDIUM, LOW std::string description; std::string cve; }; struct SecurityAssessment { std::string device_address; std::string risk_level; // HIGH, MEDIUM, LOW, NONE std::vector vulnerabilities; std::vector recommendations; }; // Apple Continuity Data Structure struct AppleContinuityData { std::string device_type; std::string device_model; int battery_level; bool battery_charging; bool airpods_in_ear; bool airpods_case_lid_open; std::string airpods_case_battery; std::string airpods_left_battery; std::string airpods_right_battery; std::string proximity_pairing_status; std::string handoff_data; std::string nearby_action; std::string raw_data; }; // Decode Apple Continuity/Proximity data AppleContinuityData decode_apple_continuity(const SimpleBLE::ByteArray& data) { AppleContinuityData result; result.battery_level = -1; result.battery_charging = false; result.airpods_in_ear = false; result.airpods_case_lid_open = false; if (data.empty()) return result; // Convert to hex string for analysis std::stringstream hex_stream; for (uint8_t byte : data) { hex_stream << std::hex << std::setw(2) << std::setfill('0') << (int)byte; } result.raw_data = hex_stream.str(); // Apple uses different message types in manufacturer data if (data.size() < 2) return result; uint8_t message_type = data[0]; // Type 0x02: Handoff if (message_type == 0x02) { result.handoff_data = "Handoff enabled (clipboard, call switching)"; } // Type 0x05: AirDrop else if (message_type == 0x05) { result.handoff_data = "AirDrop discoverable"; } // Type 0x07: AirPods/AirTag else if (message_type == 0x07 && data.size() >= 27) { // AirPods status result.device_type = "AirPods"; uint8_t status = data[6]; result.airpods_in_ear = (status & 0x01) != 0; result.airpods_case_lid_open = (status & 0x02) != 0; // Battery levels (4-bit each, 0-10 scale) uint8_t battery_data = data[7]; result.battery_level = (battery_data >> 4) * 10; // Left AirPod if (data.size() >= 10) { int left = (data[8] >> 4) & 0x0F; int right = data[8] & 0x0F; int case_battery = (data[9] >> 4) & 0x0F; if (left <= 10) result.airpods_left_battery = std::to_string(left * 10) + "%"; if (right <= 10) result.airpods_right_battery = std::to_string(right * 10) + "%"; if (case_battery <= 10) result.airpods_case_battery = std::to_string(case_battery * 10) + "%"; } } // Type 0x09: AirPlay else if (message_type == 0x09) { result.handoff_data = "AirPlay target available"; } // Type 0x0C: Proximity Pairing else if (message_type == 0x0C && data.size() >= 3) { result.proximity_pairing_status = "Proximity pairing active"; uint8_t device_type_byte = data[2]; switch (device_type_byte) { case 0x01: result.device_type = "iPhone"; break; case 0x02: result.device_type = "iPad"; break; case 0x03: result.device_type = "Apple Watch"; break; case 0x0E: result.device_type = "AirPods"; break; case 0x0F: result.device_type = "AirPods Pro"; break; case 0x13: result.device_type = "AirPods Max"; break; default: result.device_type = "Unknown Apple Device"; break; } } // Type 0x10: Nearby Action else if (message_type == 0x10) { result.nearby_action = "Device requesting action (AirTag, FindMy)"; } return result; } // Check if service list contains a specific service bool has_service(const std::vector& services, const std::string& target) { for (const auto& service : services) { std::string lower_service = service; std::string lower_target = target; std::transform(lower_service.begin(), lower_service.end(), lower_service.begin(), ::tolower); std::transform(lower_target.begin(), lower_target.end(), lower_target.begin(), ::tolower); if (lower_service.find(lower_target) != std::string::npos) { return true; } } return false; } // Assess device security based on services and characteristics SecurityAssessment assess_device_security( const std::string& address, const std::vector& services, bool is_paired, bool is_connectable, const std::string& device_name ) { SecurityAssessment assessment; assessment.device_address = address; // Check for OBEX services (HIGH risk - file transfer) if (has_service(services, "obex object push") || has_service(services, "1105")) { assessment.vulnerabilities.push_back({ "Unsecured File Transfer", "HIGH", "Device accepts file transfers without authentication", "CVE-2020-13543" }); assessment.recommendations.push_back("Disable OBEX Object Push service"); assessment.recommendations.push_back("Enable PIN protection for file transfers"); } if (has_service(services, "obex file transfer") || has_service(services, "1106")) { assessment.vulnerabilities.push_back({ "File System Access", "HIGH", "Full filesystem access via OBEX FTP", "" }); assessment.recommendations.push_back("Disable OBEX File Transfer service"); assessment.recommendations.push_back("Restrict filesystem access permissions"); } // Check for Serial Port service (MEDIUM risk) if (has_service(services, "serial port") || has_service(services, "1101")) { assessment.vulnerabilities.push_back({ "Open Serial Port", "MEDIUM", "Serial port service available without authentication", "" }); assessment.recommendations.push_back("Disable unused serial port services"); assessment.recommendations.push_back("Require authentication for SPP connections"); } // Check for HID service (MEDIUM risk - input injection) if (has_service(services, "human interface device") || has_service(services, "1812")) { assessment.vulnerabilities.push_back({ "HID Service Exposed", "MEDIUM", "HID service allows input injection attacks", "" }); assessment.recommendations.push_back("Verify HID pairing before accepting connections"); assessment.recommendations.push_back("Disable HID if not needed"); } // Check for Audio services (LOW risk - eavesdropping) if (has_service(services, "audio") || has_service(services, "110a") || has_service(services, "110b")) { assessment.vulnerabilities.push_back({ "Audio Streaming", "LOW", "Audio service allows unauthorized streaming", "" }); assessment.recommendations.push_back("Require pairing for audio connections"); assessment.recommendations.push_back("Set device to non-discoverable when not in use"); } // Check for unpaired but connectable devices (LOW risk) if (!is_paired && is_connectable) { assessment.vulnerabilities.push_back({ "Discoverable Mode", "LOW", "Device is discoverable and not paired", "" }); assessment.recommendations.push_back("Set device to non-discoverable mode"); assessment.recommendations.push_back("Enable pairing timeout"); } // Check for Android devices (potential BlueBorne) std::string lower_name = device_name; std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower); if (lower_name.find("android") != std::string::npos && services.size() > 5) { assessment.vulnerabilities.push_back({ "Potential BlueBorne Vulnerability", "HIGH", "Android device may be vulnerable to RCE if unpatched", "CVE-2017-1000251" }); assessment.recommendations.push_back("Update device to latest firmware immediately"); assessment.recommendations.push_back("Apply BlueBorne security patches"); } // Calculate overall risk level bool has_high = false; bool has_medium = false; for (const auto& vuln : assessment.vulnerabilities) { if (vuln.severity == "HIGH") has_high = true; if (vuln.severity == "MEDIUM") has_medium = true; } if (has_high) { assessment.risk_level = "HIGH"; } else if (has_medium) { assessment.risk_level = "MEDIUM"; } else if (!assessment.vulnerabilities.empty()) { assessment.risk_level = "LOW"; } else { assessment.risk_level = "NONE"; } // Add general recommendations for high/medium risk if (assessment.risk_level == "HIGH" || assessment.risk_level == "MEDIUM") { assessment.recommendations.push_back("Disable Bluetooth when not in use"); assessment.recommendations.push_back("Keep device firmware updated"); } // Remove duplicate recommendations std::sort(assessment.recommendations.begin(), assessment.recommendations.end()); assessment.recommendations.erase( std::unique(assessment.recommendations.begin(), assessment.recommendations.end()), assessment.recommendations.end() ); return assessment; } // Passive vulnerability assessment (no connection required) SecurityAssessment assess_device_passive( const std::string& address, const std::string& device_name, const std::string& company_name, bool is_connectable, const std::string& manufacturer_data ) { SecurityAssessment assessment; assessment.device_address = address; std::string lower_name = device_name; std::string lower_company = company_name; std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower); std::transform(lower_company.begin(), lower_company.end(), lower_company.begin(), ::tolower); // Known vulnerable devices by name/manufacturer if (lower_name.find("android") != std::string::npos) { assessment.vulnerabilities.push_back({ "Potential BlueBorne Target", "HIGH", "Android device potentially vulnerable to BlueBorne if unpatched (2017)", "CVE-2017-1000251" }); assessment.recommendations.push_back("Update Android to latest security patches"); assessment.recommendations.push_back("Verify BlueBorne fix is applied"); } // Old Samsung devices if (lower_company.find("samsung") != std::string::npos && (lower_name.find("galaxy s") != std::string::npos || lower_name.find("galaxy note") != std::string::npos)) { assessment.vulnerabilities.push_back({ "Legacy Samsung Device", "MEDIUM", "Older Samsung devices had Bluetooth vulnerabilities in 2019-2020", "CVE-2019-16400" }); assessment.recommendations.push_back("Check for firmware updates"); } // Apple devices with older iOS if (lower_company.find("apple") != std::string::npos && (lower_name.find("iphone") != std::string::npos || lower_name.find("ipad") != std::string::npos)) { assessment.vulnerabilities.push_back({ "Apple BLE Privacy Concern", "LOW", "Apple devices may leak tracking information via BLE advertising", "" }); assessment.recommendations.push_back("Keep iOS updated"); assessment.recommendations.push_back("Disable Bluetooth when not in use"); } // Fitness trackers - common privacy issues if (lower_name.find("fitbit") != std::string::npos || lower_name.find("garmin") != std::string::npos || lower_name.find("xiaomi") != std::string::npos) { assessment.vulnerabilities.push_back({ "Fitness Tracker Privacy", "LOW", "Fitness devices may broadcast health data in clear text", "" }); assessment.recommendations.push_back("Check privacy settings in companion app"); } // IoT devices - typically less secure if (lower_name.find("smart") != std::string::npos || lower_name.find("lock") != std::string::npos || lower_name.find("sensor") != std::string::npos) { assessment.vulnerabilities.push_back({ "IoT Device Security", "MEDIUM", "IoT devices often have outdated firmware and weak security", "" }); assessment.recommendations.push_back("Update firmware regularly"); assessment.recommendations.push_back("Check manufacturer security bulletins"); } // Devices that are connectable but not bonded - easy targets if (is_connectable) { assessment.vulnerabilities.push_back({ "Connectable Without Pairing", "LOW", "Device accepts connections without requiring pairing first", "" }); assessment.recommendations.push_back("Enable pairing requirements"); assessment.recommendations.push_back("Set device to non-discoverable when not in use"); } // Check for weak random address (privacy issue) if (address.find("00:00:00") != std::string::npos || address.find("FF:FF:FF") != std::string::npos) { assessment.vulnerabilities.push_back({ "Weak Random Address", "LOW", "Device may not properly randomize Bluetooth address", "" }); assessment.recommendations.push_back("Check device privacy settings"); } // Calculate risk level bool has_high = false; bool has_medium = false; for (const auto& vuln : assessment.vulnerabilities) { if (vuln.severity == "HIGH") has_high = true; if (vuln.severity == "MEDIUM") has_medium = true; } if (has_high) { assessment.risk_level = "HIGH"; } else if (has_medium) { assessment.risk_level = "MEDIUM"; } else if (!assessment.vulnerabilities.empty()) { assessment.risk_level = "LOW"; } else { assessment.risk_level = "NONE"; } // Remove duplicate recommendations std::sort(assessment.recommendations.begin(), assessment.recommendations.end()); assessment.recommendations.erase( std::unique(assessment.recommendations.begin(), assessment.recommendations.end()), assessment.recommendations.end() ); return assessment; } // Active vulnerability assessment (attempts connection) SecurityAssessment assess_device_active( sqlite3* db, SimpleBLE::Peripheral& peripheral, const SecurityAssessment& passive_assessment ) { SecurityAssessment assessment = passive_assessment; std::string address = peripheral.address(); try { std::cout << "[ACTIVE SCAN] Testing " << address << " for vulnerabilities..." << std::endl; // Test 1: Can we connect without pairing? bool connected_unpaired = false; if (!peripheral.is_paired()) { try { peripheral.connect(); if (peripheral.is_connected()) { connected_unpaired = true; assessment.vulnerabilities.push_back({ "Connects Without Pairing", "MEDIUM", "Device accepts connections without requiring pairing/authentication", "" }); assessment.recommendations.push_back("Require pairing before allowing connections"); std::cout << "[ACTIVE SCAN] ✓ Connected without pairing" << std::endl; } } catch (...) { std::cout << "[ACTIVE SCAN] ✗ Connection requires pairing" << std::endl; } } // Test 2: Enumerate services (if connected) if (peripheral.is_connected()) { try { auto services = peripheral.services(); std::vector service_names; for (auto& service : services) { std::string service_uuid = service.uuid(); std::string service_name = decode_service_uuid(service_uuid); if (!service_name.empty()) { service_names.push_back(service_name); } service_names.push_back(service_uuid); // Check for OBEX services if (service_uuid.find("1105") != std::string::npos || service_uuid.find("1106") != std::string::npos) { assessment.vulnerabilities.push_back({ "OBEX Service Accessible", "HIGH", "File transfer service accessible without authentication", "CVE-2020-13543" }); assessment.recommendations.push_back("Disable OBEX services"); std::cout << "[ACTIVE SCAN] ✓ OBEX service found" << std::endl; } // Check for Serial Port if (service_uuid.find("1101") != std::string::npos) { assessment.vulnerabilities.push_back({ "Serial Port Accessible", "MEDIUM", "Serial port service accessible without authentication", "" }); assessment.recommendations.push_back("Disable serial port or require authentication"); std::cout << "[ACTIVE SCAN] ✓ Serial Port found" << std::endl; } // Check for HID if (service_uuid.find("1812") != std::string::npos) { assessment.vulnerabilities.push_back({ "HID Service Accessible", "MEDIUM", "Keyboard/mouse service accessible - potential input injection", "" }); assessment.recommendations.push_back("Verify HID pairing requirements"); std::cout << "[ACTIVE SCAN] ✓ HID service found" << std::endl; } } // Test 3: Try to read characteristics without pairing if (connected_unpaired && !services.empty()) { int readable_without_auth = 0; for (auto& service : services) { auto characteristics = service.characteristics(); for (auto& characteristic : characteristics) { if (characteristic.can_read()) { try { SimpleBLE::ByteArray data = peripheral.read( SimpleBLE::BluetoothUUID(service.uuid()), SimpleBLE::BluetoothUUID(characteristic.uuid()) ); readable_without_auth++; } catch (...) { // Read failed - good, requires auth } } } } if (readable_without_auth > 0) { assessment.vulnerabilities.push_back({ "Unauthenticated Characteristic Access", "HIGH", std::string("Can read ") + std::to_string(readable_without_auth) + " characteristics without authentication", "" }); assessment.recommendations.push_back("Enable authentication for all characteristics"); std::cout << "[ACTIVE SCAN] ✓ " << readable_without_auth << " characteristics readable without auth" << std::endl; } } } catch (const std::exception& e) { std::cerr << "[ACTIVE SCAN] Error enumerating services: " << e.what() << std::endl; } // Disconnect after testing try { peripheral.disconnect(); } catch (...) {} } std::cout << "[ACTIVE SCAN] Complete for " << address << std::endl; } catch (const std::exception& e) { std::cerr << "[ACTIVE SCAN] Error: " << e.what() << std::endl; } // Recalculate risk level bool has_high = false; bool has_medium = false; for (const auto& vuln : assessment.vulnerabilities) { if (vuln.severity == "HIGH") has_high = true; if (vuln.severity == "MEDIUM") has_medium = true; } if (has_high) { assessment.risk_level = "HIGH"; } else if (has_medium) { assessment.risk_level = "MEDIUM"; } else if (!assessment.vulnerabilities.empty()) { assessment.risk_level = "LOW"; } else { assessment.risk_level = "NONE"; } // Remove duplicate vulnerabilities and recommendations std::sort(assessment.recommendations.begin(), assessment.recommendations.end()); assessment.recommendations.erase( std::unique(assessment.recommendations.begin(), assessment.recommendations.end()), assessment.recommendations.end() ); return assessment; } // Classic Bluetooth vulnerability assessment SecurityAssessment assess_classic_device(const ClassicBluetooth::ClassicDevice& device) { SecurityAssessment assessment; assessment.device_address = device.address; assessment.risk_level = "NONE"; // Check for OBEX services (file transfer without auth) for (const auto& service : device.services) { if (service.uuid == "1105") { // OBEX Object Push Vulnerability vuln; vuln.name = "OBEX Object Push Service"; vuln.severity = "HIGH"; vuln.description = "Device accepts file transfers without authentication via OBEX Object Push"; vuln.cve = "CVE-2020-13543"; assessment.vulnerabilities.push_back(vuln); assessment.recommendations.push_back("Disable OBEX Object Push service"); assessment.recommendations.push_back("Enable PIN protection for file transfers"); } if (service.uuid == "1106") { // OBEX File Transfer Vulnerability vuln; vuln.name = "OBEX File Transfer Service"; vuln.severity = "HIGH"; vuln.description = "Full filesystem access available via OBEX FTP without proper authentication"; vuln.cve = "CVE-2020-13543"; assessment.vulnerabilities.push_back(vuln); assessment.recommendations.push_back("Disable OBEX File Transfer service"); assessment.recommendations.push_back("Restrict filesystem access permissions"); } if (service.uuid == "1101") { // Serial Port Profile (SPP) Vulnerability vuln; vuln.name = "Serial Port Profile Exposed"; vuln.severity = "MEDIUM"; vuln.description = "Serial port service allows unauthorized command execution"; assessment.vulnerabilities.push_back(vuln); assessment.recommendations.push_back("Disable unused Serial Port services"); assessment.recommendations.push_back("Require authentication for SPP connections"); } if (service.uuid == "1812") { // HID Service Vulnerability vuln; vuln.name = "HID Service Accessible"; vuln.severity = "MEDIUM"; vuln.description = "Human Interface Device service allows input injection attacks"; assessment.vulnerabilities.push_back(vuln); assessment.recommendations.push_back("Verify HID pairing before accepting connections"); assessment.recommendations.push_back("Disable HID if not needed"); } if (service.uuid == "1103") { // Dialup Networking Vulnerability vuln; vuln.name = "Dialup Networking Service"; vuln.severity = "LOW"; vuln.description = "Legacy DUN service may have security weaknesses"; assessment.vulnerabilities.push_back(vuln); assessment.recommendations.push_back("Disable DUN if not actively used"); } } // Check for Android devices (BlueBorne risk) if (device.device_class.major_class == "Phone" && device.device_class.minor_class == "Smartphone") { // Check if device name suggests Android std::string name_lower = device.name; std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(), ::tolower); if (name_lower.find("android") != std::string::npos || name_lower.find("galaxy") != std::string::npos || name_lower.find("pixel") != std::string::npos) { Vulnerability vuln; vuln.name = "Potential BlueBorne Target"; vuln.severity = "HIGH"; vuln.description = "Android device may be vulnerable to BlueBorne remote code execution if unpatched"; vuln.cve = "CVE-2017-1000251"; assessment.vulnerabilities.push_back(vuln); assessment.recommendations.push_back("Update Android to latest security patches"); assessment.recommendations.push_back("Verify BlueBorne patches are applied"); } } // Check for IoT/Smart devices if (device.device_class.major_class == "Peripheral" || device.device_class.major_class == "Toy") { Vulnerability vuln; vuln.name = "IoT Device Security Risk"; vuln.severity = "MEDIUM"; vuln.description = "IoT and peripheral devices often have outdated firmware and weak security"; assessment.vulnerabilities.push_back(vuln); assessment.recommendations.push_back("Check for firmware updates"); assessment.recommendations.push_back("Review device pairing and authentication settings"); } // Calculate risk level for (const auto& vuln : assessment.vulnerabilities) { if (vuln.severity == "HIGH") { assessment.risk_level = "HIGH"; break; } else if (vuln.severity == "MEDIUM" && assessment.risk_level != "HIGH") { assessment.risk_level = "MEDIUM"; } else if (vuln.severity == "LOW" && assessment.risk_level == "NONE") { assessment.risk_level = "LOW"; } } // Add general recommendations for Classic BT if (!assessment.vulnerabilities.empty()) { assessment.recommendations.push_back("Disable Bluetooth when not in use"); assessment.recommendations.push_back("Keep device firmware updated"); assessment.recommendations.push_back("Set device to non-discoverable mode when not pairing"); } // Remove duplicate recommendations std::sort(assessment.recommendations.begin(), assessment.recommendations.end()); assessment.recommendations.erase( std::unique(assessment.recommendations.begin(), assessment.recommendations.end()), assessment.recommendations.end() ); return assessment; } // Store security assessment in database void store_security_assessment(sqlite3* db, const SecurityAssessment& assessment) { std::lock_guard lock(db_mutex); // Convert vulnerabilities to JSON string std::stringstream vulns_json; vulns_json << "["; for (size_t i = 0; i < assessment.vulnerabilities.size(); i++) { if (i > 0) vulns_json << ","; vulns_json << "{\"name\":\"" << assessment.vulnerabilities[i].name << "\"," << "\"severity\":\"" << assessment.vulnerabilities[i].severity << "\"," << "\"description\":\"" << assessment.vulnerabilities[i].description << "\""; if (!assessment.vulnerabilities[i].cve.empty()) { vulns_json << ",\"cve\":\"" << assessment.vulnerabilities[i].cve << "\""; } vulns_json << "}"; } vulns_json << "]"; // Convert recommendations to JSON array std::stringstream recs_json; recs_json << "["; for (size_t i = 0; i < assessment.recommendations.size(); i++) { if (i > 0) recs_json << ","; recs_json << "\"" << assessment.recommendations[i] << "\""; } recs_json << "]"; const char* upsert_sql = R"( INSERT INTO security_assessments (device_address, risk_level, vulnerabilities, recommendations, last_assessed) VALUES (?, ?, ?, ?, datetime('now')) ON CONFLICT(device_address) DO UPDATE SET risk_level = excluded.risk_level, vulnerabilities = excluded.vulnerabilities, recommendations = excluded.recommendations, last_assessed = datetime('now'); )"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, upsert_sql, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, assessment.device_address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, assessment.risk_level.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, vulns_json.str().c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, recs_json.str().c_str(), -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } } // Map company ID to company name std::string get_company_name(uint16_t company_id) { static const std::map companies = { {0x0001, "Nokia"}, {0x0002, "Intel"}, {0x0004, "TDK"}, {0x0006, "Microsoft"}, {0x0007, "Lucent"}, {0x0008, "Motorola"}, {0x0009, "Infineon"}, {0x000A, "Qualcomm"}, {0x000B, "Silicon Wave"}, {0x000C, "Digianswer"}, {0x000D, "Texas Instruments"}, {0x000F, "Broadcom"}, {0x0012, "CSR"}, {0x0013, "Atmel"}, {0x0014, "Mitsubishi"}, {0x0015, "RTX"}, {0x0018, "GCT Semiconductor"}, {0x0019, "Marvell"}, {0x001D, "Sony"}, {0x001E, "Hanback"}, {0x0020, "ST Microelectronics"}, {0x0025, "NXP"}, {0x002E, "Plantronics"}, {0x0030, "Dynastream"}, {0x0031, "Timex"}, {0x0039, "Bose"}, {0x003A, "Sony Ericsson"}, {0x003C, "Harman"}, {0x003D, "NXP Semiconductors"}, {0x0046, "MediaTek"}, {0x004C, "Apple"}, {0x004E, "LG Electronics"}, {0x0050, "Huawei"}, {0x005B, "Toshiba"}, {0x005D, "Polar Electro"}, {0x0067, "HP"}, {0x0075, "Samsung"}, {0x007C, "HTC"}, {0x0080, "Lenovo"}, {0x00E0, "Google"}, {0x00E5, "Garmin"}, {0x0106, "Xiaomi"}, {0x0117, "Fitbit"}, {0x0131, "Tile"}, {0x0157, "Xiaomi"}, {0x0171, "Amazon"}, {0x01A0, "OnePlus"}, {0x0276, "Fitbit"}, {0x02E5, "Garmin"}, {0x0499, "Ruuvi Innovations"} }; auto it = companies.find(company_id); return (it != companies.end()) ? it->second : "Unknown"; } // Calculate estimated distance from TX power and RSSI double calculate_distance(int16_t tx_power, int16_t rssi) { // If TX power is invalid or not advertised, use RSSI-only estimation if (tx_power == -32768 || tx_power == 0) { // Empirical RSSI to distance mapping (calibrated for typical BLE devices at 0 dBm TX) // Based on real-world measurements if (rssi >= -35) return 0.5; // Immediate proximity (< 0.5m) if (rssi >= -45) return 1.0; // Very close (0.5-1m) if (rssi >= -55) return 2.0; // Close (1-2m) if (rssi >= -65) return 5.0; // Near (2-5m) if (rssi >= -75) return 10.0; // Medium (5-10m) if (rssi >= -85) return 20.0; // Far (10-20m) return 30.0; // Very far (>20m) } // If TX power is available, use path loss formula // Formula: distance = 10 ^ ((TX_Power - RSSI) / (10 * N)) // N = 2.0 for free space, 3.0-4.0 for indoor with obstacles const double N = 2.0; // More accurate for typical indoor environments double distance = std::pow(10.0, (tx_power - rssi) / (10.0 * N)); return distance; } // Get address type as string std::string get_address_type_string(SimpleBLE::BluetoothAddressType type) { switch (type) { case SimpleBLE::BluetoothAddressType::PUBLIC: return "PUBLIC"; case SimpleBLE::BluetoothAddressType::RANDOM: return "RANDOM"; default: return "UNSPECIFIED"; } } // Enumerate characteristics for a connected device bool enumerate_characteristics(sqlite3* db, SimpleBLE::Peripheral& peripheral) { std::lock_guard lock(db_mutex); std::string address = peripheral.address(); std::string name = peripheral.identifier(); try { auto services = peripheral.services(); for (auto& service : services) { std::string service_uuid = service.uuid(); auto characteristics = service.characteristics(); for (auto& characteristic : characteristics) { std::string char_uuid = characteristic.uuid(); // Build properties string std::stringstream props; bool can_read = characteristic.can_read(); bool can_write = characteristic.can_write_request() || characteristic.can_write_command(); bool can_notify = characteristic.can_notify(); bool can_indicate = characteristic.can_indicate(); if (can_read) props << "READ,"; if (can_write) props << "WRITE,"; if (can_notify) props << "NOTIFY,"; if (can_indicate) props << "INDICATE,"; std::string properties = props.str(); if (!properties.empty()) { properties.pop_back(); // Remove trailing comma } // Insert into database const char* insert_sql = R"( INSERT OR REPLACE INTO device_characteristics (device_address, device_name, service_uuid, characteristic_uuid, properties) VALUES (?, ?, ?, ?, ?); )"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, nullptr); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, service_uuid.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, char_uuid.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 5, properties.c_str(), -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } } } std::cout << "[" << get_timestamp() << "] Enumerated characteristics for " << address << std::endl; return true; } catch (const std::exception& e) { std::cerr << "Error enumerating characteristics for " << address << ": " << e.what() << std::endl; return false; } } // Connect to device and enumerate characteristics (runs in background) void connect_and_enumerate(sqlite3* db, SimpleBLE::Peripheral peripheral) { std::string address = peripheral.address(); try { std::cout << "[" << get_timestamp() << "] Attempting to connect to " << address << "..." << std::endl; peripheral.connect(); if (peripheral.is_connected()) { std::cout << "[" << get_timestamp() << "] Connected to " << address << std::endl; enumerate_characteristics(db, peripheral); // Run ACTIVE security assessment after enumeration try { // First, get passive assessment from database SecurityAssessment passive_assessment; passive_assessment.device_address = address; passive_assessment.risk_level = "NONE"; // Load existing passive assessment if available { std::lock_guard lock(db_mutex); const char* query = "SELECT risk_level, vulnerabilities FROM security_assessments WHERE device_address = ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) == SQLITE_ROW) { passive_assessment.risk_level = reinterpret_cast(sqlite3_column_text(stmt, 0)); // Parse vulnerabilities JSON if needed } sqlite3_finalize(stmt); } } // Perform active assessment SecurityAssessment assessment = assess_device_active(db, peripheral, passive_assessment); store_security_assessment(db, assessment); std::cout << "[" << get_timestamp() << "] Active security assessment: " << address << " - Risk: " << assessment.risk_level << " (" << assessment.vulnerabilities.size() << " vulnerabilities)" << std::endl; // Try to extract additional tracking information try { std::cout << "[INFO EXTRACT] Attempting to read device information..." << std::endl; std::string manufacturer_name = ""; std::string model_number = ""; std::string firmware_version = ""; std::string hardware_version = ""; std::string serial_number = ""; // Look for Device Information Service (0x180A) auto services = peripheral.services(); for (auto& service : services) { std::string service_uuid = service.uuid(); // Device Information Service if (service_uuid.find("180a") != std::string::npos || service_uuid.find("180A") != std::string::npos) { auto characteristics = service.characteristics(); for (auto& characteristic : characteristics) { std::string char_uuid = characteristic.uuid(); try { if (characteristic.can_read()) { SimpleBLE::ByteArray data = peripheral.read( SimpleBLE::BluetoothUUID(service.uuid()), SimpleBLE::BluetoothUUID(characteristic.uuid()) ); std::string value(data.begin(), data.end()); // Manufacturer Name (0x2A29) if (char_uuid.find("2a29") != std::string::npos || char_uuid.find("2A29") != std::string::npos) { manufacturer_name = value; std::cout << "[INFO EXTRACT] Manufacturer: " << value << std::endl; } // Model Number (0x2A24) else if (char_uuid.find("2a24") != std::string::npos || char_uuid.find("2A24") != std::string::npos) { model_number = value; std::cout << "[INFO EXTRACT] Model: " << value << std::endl; } // Firmware Version (0x2A26) else if (char_uuid.find("2a26") != std::string::npos || char_uuid.find("2A26") != std::string::npos) { firmware_version = value; std::cout << "[INFO EXTRACT] Firmware: " << value << std::endl; } // Hardware Version (0x2A27) else if (char_uuid.find("2a27") != std::string::npos || char_uuid.find("2A27") != std::string::npos) { hardware_version = value; std::cout << "[INFO EXTRACT] Hardware: " << value << std::endl; } // Serial Number (0x2A25) else if (char_uuid.find("2a25") != std::string::npos || char_uuid.find("2A25") != std::string::npos) { serial_number = value; std::cout << "[INFO EXTRACT] Serial: " << value << std::endl; } } } catch (...) { // Some characteristics may require pairing/encryption } } } } // Store tracking data { std::lock_guard lock(db_mutex); const char* upsert_sql = R"( INSERT INTO device_tracking_data (device_address, manufacturer_name, device_model, firmware_version, hardware_version, serial_number, last_updated) VALUES (?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(device_address) DO UPDATE SET manufacturer_name = COALESCE(excluded.manufacturer_name, manufacturer_name), device_model = COALESCE(excluded.device_model, device_model), firmware_version = COALESCE(excluded.firmware_version, firmware_version), hardware_version = COALESCE(excluded.hardware_version, hardware_version), serial_number = COALESCE(excluded.serial_number, serial_number), last_updated = datetime('now'); )"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, upsert_sql, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, manufacturer_name.empty() ? nullptr : manufacturer_name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, model_number.empty() ? nullptr : model_number.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, firmware_version.empty() ? nullptr : firmware_version.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 5, hardware_version.empty() ? nullptr : hardware_version.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 6, serial_number.empty() ? nullptr : serial_number.c_str(), -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } } } catch (const std::exception& e) { std::cerr << "[INFO EXTRACT] Error: " << e.what() << std::endl; } } catch (const std::exception& e) { std::cerr << "Error assessing security for " << address << ": " << e.what() << std::endl; } std::this_thread::sleep_for(std::chrono::seconds(1)); peripheral.disconnect(); std::cout << "[" << get_timestamp() << "] Disconnected from " << address << std::endl; } } catch (const std::exception& e) { std::cerr << "Error connecting to " << address << ": " << e.what() << std::endl; } } // Get current timestamp in ISO 8601 format std::string get_timestamp() { auto now = std::chrono::system_clock::now(); auto time_t_now = std::chrono::system_clock::to_time_t(now); auto ms = std::chrono::duration_cast( now.time_since_epoch()) % 1000; std::stringstream ss; ss << std::put_time(std::localtime(&time_t_now), "%Y-%m-%d %H:%M:%S"); ss << '.' << std::setfill('0') << std::setw(3) << ms.count(); return ss.str(); } // Initialize SQLite database bool init_database(sqlite3** db) { int rc = sqlite3_open(config.db_path.c_str(), db); if (rc != SQLITE_OK) { std::cerr << "Cannot open database: " << sqlite3_errmsg(*db) << std::endl; return false; } const char* create_table_sql = R"( CREATE TABLE IF NOT EXISTS ble_scans ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, device_address TEXT NOT NULL, device_name TEXT, rssi INTEGER, manufacturer_data TEXT, scan_date DATE, scan_time TIME, scan_hour INTEGER, scan_minute INTEGER, address_type TEXT, tx_power INTEGER, mtu INTEGER, is_connectable BOOLEAN, service_uuids TEXT, company_id INTEGER, company_name TEXT, estimated_distance REAL, protocol_type TEXT DEFAULT 'BLE', device_class_major TEXT, device_class_minor TEXT, device_class_cod INTEGER, sdp_services TEXT ); CREATE INDEX IF NOT EXISTS idx_timestamp ON ble_scans(timestamp); CREATE INDEX IF NOT EXISTS idx_scan_date ON ble_scans(scan_date); CREATE INDEX IF NOT EXISTS idx_scan_time ON ble_scans(scan_time); CREATE INDEX IF NOT EXISTS idx_device_address ON ble_scans(device_address); CREATE INDEX IF NOT EXISTS idx_scan_hour ON ble_scans(scan_hour); CREATE INDEX IF NOT EXISTS idx_company_name ON ble_scans(company_name); CREATE TABLE IF NOT EXISTS device_distance_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, device_address TEXT NOT NULL, device_name TEXT, rssi INTEGER, tx_power INTEGER, estimated_distance REAL, scan_date DATE, scan_hour INTEGER ); CREATE INDEX IF NOT EXISTS idx_distance_timestamp ON device_distance_history(timestamp); CREATE INDEX IF NOT EXISTS idx_distance_device ON device_distance_history(device_address); CREATE INDEX IF NOT EXISTS idx_distance_date ON device_distance_history(scan_date); CREATE TABLE IF NOT EXISTS device_characteristics ( id INTEGER PRIMARY KEY AUTOINCREMENT, discovered_at DATETIME DEFAULT CURRENT_TIMESTAMP, device_address TEXT NOT NULL, device_name TEXT, service_uuid TEXT, characteristic_uuid TEXT, properties TEXT, UNIQUE(device_address, service_uuid, characteristic_uuid) ); CREATE INDEX IF NOT EXISTS idx_char_device ON device_characteristics(device_address); CREATE TABLE IF NOT EXISTS device_nicknames ( device_address TEXT PRIMARY KEY, nickname TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS security_assessments ( device_address TEXT PRIMARY KEY, risk_level TEXT NOT NULL, vulnerabilities TEXT, recommendations TEXT, last_assessed DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_security_risk ON security_assessments(risk_level); CREATE TABLE IF NOT EXISTS device_tracking_data ( device_address TEXT PRIMARY KEY, device_type TEXT, device_model TEXT, manufacturer_name TEXT, firmware_version TEXT, hardware_version TEXT, serial_number TEXT, apple_continuity_data TEXT, pairing_status TEXT, last_seen_location TEXT, tracking_identifiers TEXT, additional_info TEXT, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_tracking_device ON device_tracking_data(device_address); )"; char* err_msg = nullptr; rc = sqlite3_exec(*db, create_table_sql, nullptr, nullptr, &err_msg); if (rc != SQLITE_OK) { std::cerr << "SQL error: " << err_msg << std::endl; sqlite3_free(err_msg); return false; } std::cout << "Database initialized: " << config.db_path << std::endl; return true; } // Log BLE scan result to database with enhanced fields bool log_scan_result(sqlite3* db, SimpleBLE::Peripheral& peripheral) { std::lock_guard lock(db_mutex); auto now = std::chrono::system_clock::now(); auto time_t_now = std::chrono::system_clock::to_time_t(now); auto tm_now = std::localtime(&time_t_now); std::string address = peripheral.address(); std::string name = peripheral.identifier(); int rssi = peripheral.rssi(); int16_t tx_power = peripheral.tx_power(); uint16_t mtu = peripheral.mtu(); bool is_connectable = peripheral.is_connectable(); std::string address_type = get_address_type_string(peripheral.address_type()); // Extract manufacturer data std::string manufacturer_data = ""; uint16_t company_id = 0; std::string company_name = ""; auto mfg_data = peripheral.manufacturer_data(); if (!mfg_data.empty()) { std::stringstream ss; for (const auto& pair : mfg_data) { company_id = pair.first; // Use the first company ID found company_name = get_company_name(company_id); ss << std::hex << std::setfill('0') << std::setw(4) << pair.first << ":"; for (uint8_t byte : pair.second) { ss << std::hex << std::setfill('0') << std::setw(2) << (int)byte; } ss << ";"; } manufacturer_data = ss.str(); } // Get service UUIDs std::string service_uuids = ""; auto services = peripheral.services(); if (!services.empty()) { std::stringstream ss; for (size_t i = 0; i < services.size(); i++) { ss << services[i].uuid(); if (i < services.size() - 1) ss << ","; } service_uuids = ss.str(); } // Calculate estimated distance double estimated_distance = calculate_distance(tx_power, rssi); const char* insert_sql = R"( INSERT INTO ble_scans (device_address, device_name, rssi, manufacturer_data, scan_date, scan_time, scan_hour, scan_minute, address_type, tx_power, mtu, is_connectable, service_uuids, company_id, company_name, estimated_distance) VALUES (?, ?, ?, ?, date('now', 'localtime'), time('now', 'localtime'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); )"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { std::cerr << "Failed to prepare statement: " << sqlite3_errmsg(db) << std::endl; return false; } sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 3, rssi); sqlite3_bind_text(stmt, 4, manufacturer_data.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 5, tm_now->tm_hour); sqlite3_bind_int(stmt, 6, tm_now->tm_min); sqlite3_bind_text(stmt, 7, address_type.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 8, tx_power); sqlite3_bind_int(stmt, 9, mtu); sqlite3_bind_int(stmt, 10, is_connectable ? 1 : 0); sqlite3_bind_text(stmt, 11, service_uuids.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 12, company_id); sqlite3_bind_text(stmt, 13, company_name.c_str(), -1, SQLITE_TRANSIENT); if (estimated_distance >= 0) { sqlite3_bind_double(stmt, 14, estimated_distance); } else { sqlite3_bind_null(stmt, 14); } rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { std::cerr << "Failed to insert data: " << sqlite3_errmsg(db) << std::endl; return false; } // Also log to distance history if distance is available if (estimated_distance >= 0) { const char* distance_sql = R"( INSERT INTO device_distance_history (device_address, device_name, rssi, tx_power, estimated_distance, scan_date, scan_hour) VALUES (?, ?, ?, ?, ?, date('now', 'localtime'), ?); )"; sqlite3_stmt* dist_stmt; rc = sqlite3_prepare_v2(db, distance_sql, -1, &dist_stmt, nullptr); if (rc == SQLITE_OK) { sqlite3_bind_text(dist_stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(dist_stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(dist_stmt, 3, rssi); sqlite3_bind_int(dist_stmt, 4, tx_power); sqlite3_bind_double(dist_stmt, 5, estimated_distance); sqlite3_bind_int(dist_stmt, 6, tm_now->tm_hour); sqlite3_step(dist_stmt); sqlite3_finalize(dist_stmt); } } return true; } // Log Classic Bluetooth device to database bool log_classic_device(sqlite3* db, const ClassicBluetooth::ClassicDevice& device) { std::lock_guard lock(db_mutex); auto now = std::chrono::system_clock::now(); auto time_t_now = std::chrono::system_clock::to_time_t(now); auto tm_now = std::localtime(&time_t_now); // Serialize SDP services to JSON std::stringstream sdp_json; sdp_json << "["; for (size_t i = 0; i < device.services.size(); i++) { sdp_json << "{\"uuid\":\"" << device.services[i].uuid << "\"," << "\"name\":\"" << device.services[i].name << "\"," << "\"rfcomm_channel\":" << (int)device.services[i].rfcomm_channel << "}"; if (i < device.services.size() - 1) sdp_json << ","; } sdp_json << "]"; const char* insert_sql = R"( INSERT INTO ble_scans (device_address, device_name, rssi, scan_date, scan_time, scan_hour, scan_minute, is_connectable, company_name, protocol_type, device_class_major, device_class_minor, device_class_cod, sdp_services) VALUES (?, ?, ?, date('now', 'localtime'), time('now', 'localtime'), ?, ?, ?, ?, 'CLASSIC', ?, ?, ?, ?); )"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { std::cerr << "Failed to prepare statement: " << sqlite3_errmsg(db) << std::endl; return false; } sqlite3_bind_text(stmt, 1, device.address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, device.name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 3, device.rssi); sqlite3_bind_int(stmt, 4, tm_now->tm_hour); sqlite3_bind_int(stmt, 5, tm_now->tm_min); sqlite3_bind_int(stmt, 6, device.is_connected ? 1 : 0); sqlite3_bind_text(stmt, 7, "Classic Bluetooth", -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 8, device.device_class.major_class.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 9, device.device_class.minor_class.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 10, device.device_class.class_of_device); sqlite3_bind_text(stmt, 11, sdp_json.str().c_str(), -1, SQLITE_TRANSIENT); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { std::cerr << "Failed to insert Classic device: " << sqlite3_errmsg(db) << std::endl; return false; } return true; } // Perform BLE scan void perform_scan(sqlite3* db) { auto adapters = SimpleBLE::Adapter::get_adapters(); if (adapters.empty()) { std::cerr << "No Bluetooth adapters found" << std::endl; return; } auto adapter = adapters[0]; std::cout << "Using adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl; adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { std::string address = peripheral.address(); std::string name = peripheral.identifier(); int rssi = peripheral.rssi(); int16_t tx_power = peripheral.tx_power(); double distance = calculate_distance(tx_power, rssi); bool is_connectable = peripheral.is_connectable(); std::cout << "[" << get_timestamp() << "] Found: " << address << " | Name: " << (name.empty() ? "" : name) << " | RSSI: " << rssi << " dBm"; if (distance >= 0) { std::cout << " | Distance: ~" << std::fixed << std::setprecision(1) << distance << "m"; } // Extract company name and decode Apple data std::string company_name = "Unknown"; AppleContinuityData apple_data; auto mfg_data = peripheral.manufacturer_data(); if (!mfg_data.empty()) { for (const auto& pair : mfg_data) { company_name = get_company_name(pair.first); std::cout << " | " << company_name; // If Apple device, decode continuity data if (pair.first == 0x004C) { // Apple company ID apple_data = decode_apple_continuity(pair.second); // Log decoded Apple data if (!apple_data.device_type.empty()) { std::cout << " [" << apple_data.device_type << "]"; } if (apple_data.battery_level >= 0) { std::cout << " [Battery: " << apple_data.battery_level << "%]"; } if (!apple_data.airpods_left_battery.empty()) { std::cout << " [L:" << apple_data.airpods_left_battery << " R:" << apple_data.airpods_right_battery << " Case:" << apple_data.airpods_case_battery << "]"; } // Store Apple continuity data try { std::lock_guard lock(db_mutex); std::stringstream apple_json; apple_json << "{" << "\"device_type\":\"" << apple_data.device_type << "\"," << "\"battery\":" << apple_data.battery_level << "," << "\"airpods_left\":\"" << apple_data.airpods_left_battery << "\"," << "\"airpods_right\":\"" << apple_data.airpods_right_battery << "\"," << "\"airpods_case\":\"" << apple_data.airpods_case_battery << "\"," << "\"in_ear\":" << (apple_data.airpods_in_ear ? "true" : "false") << "," << "\"case_open\":" << (apple_data.airpods_case_lid_open ? "true" : "false") << "," << "\"handoff\":\"" << apple_data.handoff_data << "\"," << "\"raw\":\"" << apple_data.raw_data << "\"" << "}"; const char* upsert_sql = R"( INSERT INTO device_tracking_data (device_address, device_type, apple_continuity_data, last_updated) VALUES (?, ?, ?, datetime('now')) ON CONFLICT(device_address) DO UPDATE SET apple_continuity_data = excluded.apple_continuity_data, last_updated = datetime('now'); )"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, upsert_sql, -1, &stmt, nullptr) == SQLITE_OK) { sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, apple_data.device_type.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, apple_json.str().c_str(), -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } } catch (...) {} } break; // Show first company only } } if (is_connectable) { std::cout << " | Connectable"; } std::cout << std::endl; log_scan_result(db, peripheral); // Perform PASSIVE security assessment immediately (no connection needed) try { SecurityAssessment passive_assessment = assess_device_passive( address, peripheral.identifier(), company_name, is_connectable, "" // manufacturer_data - could extract from peripheral if available ); // Store passive assessment store_security_assessment(db, passive_assessment); std::cout << "[PASSIVE SCAN] " << address << " - Risk: " << passive_assessment.risk_level; if (!passive_assessment.vulnerabilities.empty()) { std::cout << " (" << passive_assessment.vulnerabilities.size() << " issues)"; } std::cout << std::endl; } catch (const std::exception& e) { std::cerr << "Error in passive assessment: " << e.what() << std::endl; } // Optionally connect to connectable devices we haven't enumerated yet if (is_connectable) { std::lock_guard lock(enumerated_mutex); if (enumerated_devices.find(address) == enumerated_devices.end()) { enumerated_devices.insert(address); // Launch connection in background thread std::thread([db, peripheral]() mutable { connect_and_enumerate(db, peripheral); }).detach(); } } }); // Initialize Classic Bluetooth scanner ClassicBluetooth::Scanner classic_scanner; classic_scanner.set_device_found_callback([db](const ClassicBluetooth::ClassicDevice& device) { std::cout << "[" << get_timestamp() << "] Classic BT Found: " << device.address << " | Name: " << device.name << " | RSSI: " << device.rssi << " dBm" << " | Class: " << device.device_class.major_class << " (" << device.device_class.minor_class << ")" << " | Services: " << device.services.size() << std::endl; // Perform security assessment SecurityAssessment assessment = assess_classic_device(device); if (!assessment.vulnerabilities.empty()) { std::cout << "[CLASSIC SECURITY] " << device.address << " - Risk: " << assessment.risk_level << " (" << assessment.vulnerabilities.size() << " vulnerabilities)" << std::endl; store_security_assessment(db, assessment); } // Log to database log_classic_device(db, device); }); int scan_cycle = 0; while (running) { try { // BLE scan every cycle std::cout << "\n[" << get_timestamp() << "] Starting BLE scan..." << std::endl; adapter.scan_for(config.scan_duration_ms); std::cout << "[" << get_timestamp() << "] BLE scan complete." << std::endl; // Classic Bluetooth inquiry every 3 cycles (less frequent due to longer duration) if (scan_cycle % 3 == 0) { std::cout << "[" << get_timestamp() << "] Starting Classic Bluetooth inquiry..." << std::endl; classic_scanner.start_inquiry(10); // 10 second inquiry // Wait for inquiry to complete std::this_thread::sleep_for(std::chrono::seconds(11)); std::cout << "[" << get_timestamp() << "] Classic Bluetooth inquiry complete." << std::endl; } scan_cycle++; std::cout << "[" << get_timestamp() << "] Scan cycle " << scan_cycle << " complete. Waiting..." << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(config.scan_interval_ms)); } catch (const std::exception& e) { std::cerr << "Scan error: " << e.what() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(5)); } } } // HTTP API handlers void setup_http_server(httplib::Server& server, sqlite3* db) { // Add CORS headers to all responses server.set_post_routing_handler([](const httplib::Request&, httplib::Response& res) { res.set_header("Access-Control-Allow-Origin", "*"); res.set_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.set_header("Access-Control-Allow-Headers", "Content-Type"); }); // Handle preflight OPTIONS requests for CORS server.Options(R"(/.*)", [](const httplib::Request&, httplib::Response& res) { res.status = 204; }); // Health check endpoint server.Get("/health", [](const httplib::Request&, httplib::Response& res) { res.set_content("{\"status\":\"ok\"}", "application/json"); }); // Get recent scans server.Get("/scans/recent", [db](const httplib::Request& req, httplib::Response& res) { int limit = 100; if (req.has_param("limit")) { limit = std::stoi(req.get_param_value("limit")); } std::lock_guard lock(db_mutex); std::string query = "SELECT timestamp, device_address, device_name, rssi, address_type, " "tx_power, mtu, is_connectable, service_uuids, company_name, estimated_distance, " "protocol_type, device_class_major, device_class_minor, sdp_services " "FROM ble_scans ORDER BY timestamp DESC LIMIT ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_int(stmt, 1, limit); std::stringstream json; json << "{\"scans\":["; bool first = true; while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; first = false; json << "{\"timestamp\":\"" << sqlite3_column_text(stmt, 0) << "\"," << "\"address\":\"" << sqlite3_column_text(stmt, 1) << "\"," << "\"name\":\"" << (sqlite3_column_text(stmt, 2) ? (const char*)sqlite3_column_text(stmt, 2) : "") << "\"," << "\"rssi\":" << sqlite3_column_int(stmt, 3) << "," << "\"address_type\":\"" << (sqlite3_column_text(stmt, 4) ? (const char*)sqlite3_column_text(stmt, 4) : "") << "\"," << "\"tx_power\":" << sqlite3_column_int(stmt, 5) << "," << "\"mtu\":" << sqlite3_column_int(stmt, 6) << "," << "\"is_connectable\":" << (sqlite3_column_int(stmt, 7) ? "true" : "false") << "," << "\"service_uuids\":\"" << (sqlite3_column_text(stmt, 8) ? (const char*)sqlite3_column_text(stmt, 8) : "") << "\"," << "\"company_name\":\"" << (sqlite3_column_text(stmt, 9) ? (const char*)sqlite3_column_text(stmt, 9) : "") << "\""; if (sqlite3_column_type(stmt, 10) != SQLITE_NULL) { json << ",\"distance\":" << std::fixed << std::setprecision(1) << sqlite3_column_double(stmt, 10); } json << ",\"protocol_type\":\"" << (sqlite3_column_text(stmt, 11) ? (const char*)sqlite3_column_text(stmt, 11) : "BLE") << "\""; if (sqlite3_column_type(stmt, 12) != SQLITE_NULL) { json << ",\"device_class_major\":\"" << sqlite3_column_text(stmt, 12) << "\""; } if (sqlite3_column_type(stmt, 13) != SQLITE_NULL) { json << ",\"device_class_minor\":\"" << sqlite3_column_text(stmt, 13) << "\""; } if (sqlite3_column_type(stmt, 14) != SQLITE_NULL) { json << ",\"sdp_services\":" << sqlite3_column_text(stmt, 14); } json << "}"; } json << "]}"; sqlite3_finalize(stmt); res.set_content(json.str(), "application/json"); }); // Query by date server.Get("/scans/date/:date", [db](const httplib::Request& req, httplib::Response& res) { std::string date = req.path_params.at("date"); std::lock_guard lock(db_mutex); std::string query = "SELECT timestamp, device_address, device_name, rssi FROM ble_scans " "WHERE scan_date = ? ORDER BY timestamp;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_text(stmt, 1, date.c_str(), -1, SQLITE_TRANSIENT); std::stringstream json; json << "{\"date\":\"" << date << "\",\"scans\":["; bool first = true; while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; first = false; json << "{\"timestamp\":\"" << sqlite3_column_text(stmt, 0) << "\"," << "\"address\":\"" << sqlite3_column_text(stmt, 1) << "\"," << "\"name\":\"" << sqlite3_column_text(stmt, 2) << "\"," << "\"rssi\":" << sqlite3_column_int(stmt, 3) << "}"; } json << "]}"; sqlite3_finalize(stmt); res.set_content(json.str(), "application/json"); }); // Query by hour range server.Get("/scans/hour/:hour", [db](const httplib::Request& req, httplib::Response& res) { int hour = std::stoi(req.path_params.at("hour")); std::lock_guard lock(db_mutex); std::string query = "SELECT timestamp, device_address, device_name, rssi FROM ble_scans " "WHERE scan_hour = ? ORDER BY timestamp DESC LIMIT 1000;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_int(stmt, 1, hour); std::stringstream json; json << "{\"hour\":" << hour << ",\"scans\":["; bool first = true; while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; first = false; json << "{\"timestamp\":\"" << sqlite3_column_text(stmt, 0) << "\"," << "\"address\":\"" << sqlite3_column_text(stmt, 1) << "\"," << "\"name\":\"" << sqlite3_column_text(stmt, 2) << "\"," << "\"rssi\":" << sqlite3_column_int(stmt, 3) << "}"; } json << "]}"; sqlite3_finalize(stmt); res.set_content(json.str(), "application/json"); }); // Statistics endpoint server.Get("/stats", [db](const httplib::Request&, httplib::Response& res) { std::lock_guard lock(db_mutex); std::string query = "SELECT COUNT(*) as total, " "COUNT(DISTINCT device_address) as unique_devices, " "MIN(timestamp) as first_scan, " "MAX(timestamp) as last_scan FROM ble_scans;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } std::stringstream json; if (sqlite3_step(stmt) == SQLITE_ROW) { json << "{\"total_scans\":" << sqlite3_column_int(stmt, 0) << "," << "\"unique_devices\":" << sqlite3_column_int(stmt, 1) << "," << "\"first_scan\":\"" << sqlite3_column_text(stmt, 2) << "\"," << "\"last_scan\":\"" << sqlite3_column_text(stmt, 3) << "\"}"; } sqlite3_finalize(stmt); res.set_content(json.str(), "application/json"); }); // Get device scan count server.Get("/device/:address/count", [db](const httplib::Request& req, httplib::Response& res) { std::string address = req.path_params.at("address"); std::lock_guard lock(db_mutex); std::string query = "SELECT COUNT(*) as count FROM ble_scans WHERE device_address = ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); std::stringstream json; if (sqlite3_step(stmt) == SQLITE_ROW) { json << "{\"address\":\"" << address << "\"," << "\"count\":" << sqlite3_column_int(stmt, 0) << "}"; } else { json << "{\"address\":\"" << address << "\",\"count\":0}"; } sqlite3_finalize(stmt); res.set_content(json.str(), "application/json"); }); // Get distance history for a device server.Get("/distance/:address", [db](const httplib::Request& req, httplib::Response& res) { std::string address = req.path_params.at("address"); int limit = req.has_param("limit") ? std::stoi(req.get_param_value("limit")) : 100; std::lock_guard lock(db_mutex); std::string query = "SELECT timestamp, rssi, tx_power, estimated_distance FROM device_distance_history " "WHERE device_address = ? ORDER BY timestamp DESC LIMIT ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 2, limit); std::stringstream json; json << "{\"address\":\"" << address << "\",\"history\":["; bool first = true; while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; first = false; json << "{\"timestamp\":\"" << sqlite3_column_text(stmt, 0) << "\"," << "\"rssi\":" << sqlite3_column_int(stmt, 1) << "," << "\"tx_power\":" << sqlite3_column_int(stmt, 2) << "," << "\"distance\":" << std::fixed << std::setprecision(1) << sqlite3_column_double(stmt, 3) << "}"; } json << "]}"; sqlite3_finalize(stmt); res.set_content(json.str(), "application/json"); }); // Get characteristics for a device server.Get("/characteristics/:address", [db](const httplib::Request& req, httplib::Response& res) { std::string address = req.path_params.at("address"); std::lock_guard lock(db_mutex); std::string query = "SELECT service_uuid, characteristic_uuid, properties, discovered_at " "FROM device_characteristics WHERE device_address = ? ORDER BY discovered_at DESC;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); std::stringstream json; json << "{\"address\":\"" << address << "\",\"characteristics\":["; bool first = true; while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; first = false; std::string service_uuid = reinterpret_cast(sqlite3_column_text(stmt, 0)); std::string char_uuid = reinterpret_cast(sqlite3_column_text(stmt, 1)); std::string properties = reinterpret_cast(sqlite3_column_text(stmt, 2)); std::string service_name = decode_service_uuid(service_uuid); std::string char_name = decode_characteristic_uuid(char_uuid); std::string char_desc = get_characteristic_description(char_uuid); json << "{\"service\":\"" << service_uuid << "\"," << "\"service_name\":\"" << service_name << "\"," << "\"characteristic\":\"" << char_uuid << "\"," << "\"characteristic_name\":\"" << char_name << "\"," << "\"description\":\"" << char_desc << "\"," << "\"properties\":\"" << properties << "\"," << "\"discovered_at\":\"" << sqlite3_column_text(stmt, 3) << "\"}"; } json << "]}"; sqlite3_finalize(stmt); res.set_content(json.str(), "application/json"); }); // Read characteristic value server.Get("/read/:address/:service/:characteristic", [](const httplib::Request& req, httplib::Response& res) { std::string address = req.path_params.at("address"); std::string service_uuid = req.path_params.at("service"); std::string char_uuid = req.path_params.at("characteristic"); try { auto adapters = SimpleBLE::Adapter::get_adapters(); if (adapters.empty()) { res.status = 500; res.set_content("{\"error\":\"No Bluetooth adapters found\"}", "application/json"); return; } auto adapter = adapters[0]; auto peripherals = adapter.scan_get_results(); SimpleBLE::Peripheral target_peripheral; bool found = false; for (auto& peripheral : peripherals) { if (peripheral.identifier() == address || peripheral.address() == address) { target_peripheral = peripheral; found = true; break; } } if (!found) { res.status = 404; res.set_content("{\"error\":\"Device not found or not in range\"}", "application/json"); return; } if (!target_peripheral.is_connected()) { std::cout << "[READ] Connecting to " << address << "..." << std::endl; target_peripheral.connect(); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); } SimpleBLE::BluetoothUUID service(service_uuid); SimpleBLE::BluetoothUUID characteristic(char_uuid); SimpleBLE::ByteArray data = target_peripheral.read(service, characteristic); // Disconnect after reading if (target_peripheral.is_connected()) { target_peripheral.disconnect(); } // Convert byte array to hex string and interpret based on characteristic type std::stringstream json; json << "{\"address\":\"" << address << "\"," << "\"service\":\"" << service_uuid << "\"," << "\"characteristic\":\"" << char_uuid << "\"," << "\"raw\":\""; for (uint8_t byte : data) { json << std::hex << std::setw(2) << std::setfill('0') << (int)byte; } json << "\","; // Decode specific known characteristics std::string upper_char = char_uuid; std::transform(upper_char.begin(), upper_char.end(), upper_char.begin(), ::toupper); if (upper_char.find("2A19") != std::string::npos && data.size() >= 1) { // Battery Level (0-100%) json << "\"value\":" << (int)data[0] << "," << "\"unit\":\"%\",\"type\":\"battery\""; } else if (upper_char.find("2A00") != std::string::npos || upper_char.find("2A29") != std::string::npos || upper_char.find("2A24") != std::string::npos || upper_char.find("2A26") != std::string::npos) { // String characteristics (Device Name, Manufacturer, Model, Firmware) std::string text(data.begin(), data.end()); json << "\"value\":\"" << text << "\",\"type\":\"string\""; } else if (upper_char.find("2A6E") != std::string::npos && data.size() >= 2) { // Temperature (signed 16-bit) int16_t temp = (data[1] << 8) | data[0]; json << "\"value\":" << (temp / 100.0) << "," << "\"unit\":\"°C\",\"type\":\"temperature\""; } else if (upper_char.find("2A37") != std::string::npos && data.size() >= 2) { // Heart Rate uint8_t bpm = data[1]; json << "\"value\":" << (int)bpm << "," << "\"unit\":\"bpm\",\"type\":\"heart_rate\""; } else { json << "\"value\":null,\"type\":\"unknown\""; } json << "}"; res.set_content(json.str(), "application/json"); } catch (const std::exception& e) { std::cerr << "[READ] Error: " << e.what() << std::endl; std::string error_msg = e.what(); std::stringstream json; // Check for encryption/pairing errors if (error_msg.find("Encryption") != std::string::npos || error_msg.find("insufficient") != std::string::npos || error_msg.find("Authentication") != std::string::npos) { res.status = 403; json << "{\"error\":\"Device requires pairing/bonding\"," << "\"details\":\"This characteristic is encrypted and requires you to pair with the device first. " << "Pair via System Settings → Bluetooth, then try again.\"," << "\"requires_pairing\":true}"; } else if (error_msg.find("not in range") != std::string::npos || error_msg.find("not found") != std::string::npos) { res.status = 404; json << "{\"error\":\"Device not found\"," << "\"details\":\"Device is not currently in range or not responding.\"}"; } else if (error_msg.find("timeout") != std::string::npos || error_msg.find("Timed out") != std::string::npos) { res.status = 504; json << "{\"error\":\"Connection timeout\"," << "\"details\":\"Device did not respond in time. It may be too far away or busy.\"}"; } else { res.status = 500; json << "{\"error\":\"" << error_msg << "\"}"; } res.set_content(json.str(), "application/json"); } }); // ESP32 sensor data ingestion endpoint server.Post("/esp32/scan", [db](const httplib::Request& req, httplib::Response& res) { std::lock_guard lock(db_mutex); try { // Parse JSON from ESP32 std::string body = req.body; std::cout << "[ESP32] Received data from sensor node (" << body.length() << " bytes)" << std::endl; // Very basic JSON parsing (production should use a proper JSON library) size_t device_name_pos = body.find("\"device_name\":\""); std::string device_name = "ESP32"; if (device_name_pos != std::string::npos) { size_t start = device_name_pos + 15; size_t end = body.find("\"", start); device_name = body.substr(start, end - start); } // Extract scans array size_t scans_pos = body.find("\"scans\":["); if (scans_pos == std::string::npos) { res.status = 400; res.set_content("{\"status\":\"error\",\"message\":\"No scans array found\"}", "application/json"); return; } // Count devices by looking for address fields int device_count = 0; size_t pos = 0; while ((pos = body.find("\"address\":", pos)) != std::string::npos) { device_count++; // Extract device data size_t addr_start = body.find("\"", pos + 10) + 1; size_t addr_end = body.find("\"", addr_start); std::string address = body.substr(addr_start, addr_end - addr_start); // Extract name size_t name_pos = body.find("\"name\":\"", pos); std::string name = ""; if (name_pos != std::string::npos && name_pos < pos + 500) { size_t name_start = name_pos + 8; size_t name_end = body.find("\"", name_start); name = body.substr(name_start, name_end - name_start); } // Extract RSSI size_t rssi_pos = body.find("\"rssi\":", pos); int rssi = 0; if (rssi_pos != std::string::npos && rssi_pos < pos + 500) { size_t rssi_start = rssi_pos + 7; size_t rssi_end = body.find_first_of(",}", rssi_start); rssi = std::stoi(body.substr(rssi_start, rssi_end - rssi_start)); } // Extract company_name size_t company_pos = body.find("\"company_name\":\"", pos); std::string company = ""; if (company_pos != std::string::npos && company_pos < pos + 500) { size_t company_start = company_pos + 16; size_t company_end = body.find("\"", company_start); company = body.substr(company_start, company_end - company_start); } // Insert into database with source indication const char* insert_sql = R"( INSERT INTO ble_scans (device_address, device_name, rssi, manufacturer_data, scan_date, scan_time, scan_hour, scan_minute, company_name) VALUES (?, ?, ?, ?, date('now', 'localtime'), time('now', 'localtime'), ?, ?, ?); )"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, nullptr) == SQLITE_OK) { auto now = std::chrono::system_clock::now(); auto time_t_now = std::chrono::system_clock::to_time_t(now); auto tm_now = std::localtime(&time_t_now); std::string source_data = "ESP32:" + device_name; sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 3, rssi); sqlite3_bind_text(stmt, 4, source_data.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 5, tm_now->tm_hour); sqlite3_bind_int(stmt, 6, tm_now->tm_min); sqlite3_bind_text(stmt, 7, company.c_str(), -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } pos = addr_end; } std::cout << "[ESP32] Stored " << device_count << " scans from " << device_name << std::endl; std::stringstream json; json << "{\"status\":\"ok\",\"received\":" << device_count << ",\"source\":\"" << device_name << "\"}"; res.set_content(json.str(), "application/json"); } catch (const std::exception& e) { std::cerr << "[ESP32] Error processing data: " << e.what() << std::endl; res.status = 500; res.set_content("{\"status\":\"error\",\"message\":\"Processing error\"}", "application/json"); } }); // Get device nickname server.Get("/nickname/:address", [db](const httplib::Request& req, httplib::Response& res) { std::string address = req.path_params.at("address"); std::lock_guard lock(db_mutex); std::string query = "SELECT nickname FROM device_nicknames WHERE device_address = ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) == SQLITE_ROW) { std::string nickname = reinterpret_cast(sqlite3_column_text(stmt, 0)); std::stringstream json; json << "{\"address\":\"" << address << "\",\"nickname\":\"" << nickname << "\"}"; res.set_content(json.str(), "application/json"); } else { res.status = 404; res.set_content("{\"error\":\"No nickname found\"}", "application/json"); } sqlite3_finalize(stmt); }); // Set device nickname server.Post("/nickname/:address", [db](const httplib::Request& req, httplib::Response& res) { std::string address = req.path_params.at("address"); std::string body = req.body; // Extract nickname from JSON body size_t nickname_pos = body.find("\"nickname\":\""); if (nickname_pos == std::string::npos) { res.status = 400; res.set_content("{\"error\":\"Missing nickname field\"}", "application/json"); return; } size_t nickname_start = nickname_pos + 12; size_t nickname_end = body.find("\"", nickname_start); std::string nickname = body.substr(nickname_start, nickname_end - nickname_start); std::lock_guard lock(db_mutex); const char* upsert_sql = R"( INSERT INTO device_nicknames (device_address, nickname, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(device_address) DO UPDATE SET nickname = excluded.nickname, updated_at = datetime('now'); )"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, upsert_sql, -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, nickname.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) == SQLITE_DONE) { std::stringstream json; json << "{\"status\":\"ok\",\"address\":\"" << address << "\",\"nickname\":\"" << nickname << "\"}"; res.set_content(json.str(), "application/json"); } else { res.status = 500; res.set_content("{\"error\":\"Failed to save nickname\"}", "application/json"); } sqlite3_finalize(stmt); }); // Delete device nickname server.Delete("/nickname/:address", [db](const httplib::Request& req, httplib::Response& res) { std::string address = req.path_params.at("address"); std::lock_guard lock(db_mutex); const char* delete_sql = "DELETE FROM device_nicknames WHERE device_address = ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, delete_sql, -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) == SQLITE_DONE) { res.set_content("{\"status\":\"ok\"}", "application/json"); } else { res.status = 500; res.set_content("{\"error\":\"Failed to delete nickname\"}", "application/json"); } sqlite3_finalize(stmt); }); // Get all nicknames server.Get("/nicknames", [db](const httplib::Request&, httplib::Response& res) { std::lock_guard lock(db_mutex); std::string query = "SELECT device_address, nickname FROM device_nicknames;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } std::stringstream json; json << "{\"nicknames\":{"; bool first = true; while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; first = false; std::string address = reinterpret_cast(sqlite3_column_text(stmt, 0)); std::string nickname = reinterpret_cast(sqlite3_column_text(stmt, 1)); json << "\"" << address << "\":\"" << nickname << "\""; } json << "}}"; sqlite3_finalize(stmt); res.set_content(json.str(), "application/json"); }); // Get security assessment for a device server.Get("/security/:address", [db](const httplib::Request& req, httplib::Response& res) { std::string address = req.path_params.at("address"); std::lock_guard lock(db_mutex); std::string query = "SELECT risk_level, vulnerabilities, recommendations, last_assessed " "FROM security_assessments WHERE device_address = ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) == SQLITE_ROW) { std::string risk_level = reinterpret_cast(sqlite3_column_text(stmt, 0)); std::string vulnerabilities = reinterpret_cast(sqlite3_column_text(stmt, 1)); std::string recommendations = reinterpret_cast(sqlite3_column_text(stmt, 2)); std::string last_assessed = reinterpret_cast(sqlite3_column_text(stmt, 3)); std::stringstream json; json << "{\"address\":\"" << address << "\"," << "\"risk_level\":\"" << risk_level << "\"," << "\"vulnerabilities\":" << vulnerabilities << "," << "\"recommendations\":" << recommendations << "," << "\"last_assessed\":\"" << last_assessed << "\"}"; res.set_content(json.str(), "application/json"); } else { res.status = 404; res.set_content("{\"error\":\"No security assessment found\",\"message\":\"Device not yet assessed\"}", "application/json"); } sqlite3_finalize(stmt); }); // Get all security assessments server.Get("/security", [db](const httplib::Request&, httplib::Response& res) { std::lock_guard lock(db_mutex); std::string query = "SELECT device_address, risk_level, last_assessed FROM security_assessments ORDER BY risk_level DESC;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } std::stringstream json; json << "{\"assessments\":["; bool first = true; while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) json << ","; first = false; std::string address = reinterpret_cast(sqlite3_column_text(stmt, 0)); std::string risk_level = reinterpret_cast(sqlite3_column_text(stmt, 1)); std::string last_assessed = reinterpret_cast(sqlite3_column_text(stmt, 2)); json << "{\"address\":\"" << address << "\"," << "\"risk_level\":\"" << risk_level << "\"," << "\"last_assessed\":\"" << last_assessed << "\"}"; } json << "]}"; sqlite3_finalize(stmt); res.set_content(json.str(), "application/json"); }); // Get tracking data for a device server.Get("/tracking/:address", [db](const httplib::Request& req, httplib::Response& res) { std::string address = req.path_params.at("address"); std::lock_guard lock(db_mutex); std::string query = "SELECT * FROM device_tracking_data WHERE device_address = ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { res.status = 500; res.set_content("{\"error\":\"Database error\"}", "application/json"); return; } sqlite3_bind_text(stmt, 1, address.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) == SQLITE_ROW) { std::stringstream json; json << "{\"address\":\"" << address << "\","; // Get all columns if (sqlite3_column_text(stmt, 1)) json << "\"device_type\":\"" << sqlite3_column_text(stmt, 1) << "\","; if (sqlite3_column_text(stmt, 2)) json << "\"device_model\":\"" << sqlite3_column_text(stmt, 2) << "\","; if (sqlite3_column_text(stmt, 3)) json << "\"manufacturer_name\":\"" << sqlite3_column_text(stmt, 3) << "\","; if (sqlite3_column_text(stmt, 4)) json << "\"firmware_version\":\"" << sqlite3_column_text(stmt, 4) << "\","; if (sqlite3_column_text(stmt, 5)) json << "\"hardware_version\":\"" << sqlite3_column_text(stmt, 5) << "\","; if (sqlite3_column_text(stmt, 6)) json << "\"serial_number\":\"" << sqlite3_column_text(stmt, 6) << "\","; if (sqlite3_column_text(stmt, 7)) json << "\"apple_continuity\":" << sqlite3_column_text(stmt, 7) << ","; if (sqlite3_column_text(stmt, 8)) json << "\"pairing_status\":\"" << sqlite3_column_text(stmt, 8) << "\","; if (sqlite3_column_text(stmt, 11)) json << "\"additional_info\":\"" << sqlite3_column_text(stmt, 11) << "\","; json << "\"last_updated\":\"" << sqlite3_column_text(stmt, 12) << "\"}"; res.set_content(json.str(), "application/json"); } else { res.status = 404; res.set_content("{\"error\":\"No tracking data found\"}", "application/json"); } sqlite3_finalize(stmt); }); } int main(int argc, char* argv[]) { std::cout << "Bluehound v1.0" << std::endl; std::cout << "==============\n" << std::endl; // Setup signal handlers signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); // Initialize database sqlite3* db; if (!init_database(&db)) { return 1; } // Start HTTP server in separate thread std::thread http_thread; httplib::Server server; if (config.enable_http_server) { setup_http_server(server, db); http_thread = std::thread([&server]() { std::cout << "Starting HTTP server on port " << config.http_port << std::endl; std::cout << "API Endpoints:" << std::endl; std::cout << " - GET /health" << std::endl; std::cout << " - GET /scans/recent?limit=N" << std::endl; std::cout << " - GET /scans/date/YYYY-MM-DD" << std::endl; std::cout << " - GET /scans/hour/HH" << std::endl; std::cout << " - GET /stats\n" << std::endl; server.listen("0.0.0.0", config.http_port); }); } // Start BLE scanning perform_scan(db); // Cleanup if (config.enable_http_server) { server.stop(); if (http_thread.joinable()) { http_thread.join(); } } sqlite3_close(db); std::cout << "Shutdown complete." << std::endl; return 0; }