Files
2026-07-29 00:39:00 -04:00

2498 lines
105 KiB
C++

/*
* Bluehound - Continuous BLE/Classic Bluetooth scanner with time-searchable logging
* Built for embedded devices with WiFi streaming support
*/
#include <simpleble/SimpleBLE.h>
#include <sqlite3.h>
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <iomanip>
#include <sstream>
#include <csignal>
#include <atomic>
#include <mutex>
#include <vector>
#include <set>
#include <map>
#include <cmath>
// 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<bool> running(true);
std::mutex db_mutex;
std::set<std::string> 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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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<Vulnerability> vulnerabilities;
std::vector<std::string> 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<std::string>& 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<std::string>& 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<std::string> 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<std::mutex> 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<uint16_t, std::string> 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<std::mutex> 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<std::mutex> 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<const char*>(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<std::mutex> 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<std::chrono::milliseconds>(
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<std::mutex> 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<std::mutex> 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() ? "<unnamed>" : 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<const char*>(sqlite3_column_text(stmt, 0));
std::string char_uuid = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
std::string properties = reinterpret_cast<const char*>(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<std::mutex> 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<std::mutex> 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<const char*>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<const char*>(sqlite3_column_text(stmt, 0));
std::string nickname = reinterpret_cast<const char*>(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<std::mutex> 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<const char*>(sqlite3_column_text(stmt, 0));
std::string vulnerabilities = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
std::string recommendations = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
std::string last_assessed = reinterpret_cast<const char*>(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<std::mutex> 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<const char*>(sqlite3_column_text(stmt, 0));
std::string risk_level = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
std::string last_assessed = reinterpret_cast<const char*>(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<std::mutex> 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;
}