espilon-source/tools/C3PO/hp_dashboard/hp_geo.py
Eun0us 79c2a4d4bf c3po: full server rewrite with modular routes and honeypot dashboard
Replace monolithic CLI and web server with route-based Flask API.
New routes: api_commands, api_build, api_can, api_monitor, api_ota,
api_tunnel. Add honeypot security dashboard with real-time SSE,
MITRE ATT&CK mapping, kill chain analysis.

New TUI with commander/help modules. Add session management,
tunnel proxy core, CAN bus data store. Docker support.
2026-02-28 20:12:27 +01:00

313 lines
11 KiB
Python

"""
Honeypot Geo-IP & MAC Vendor Lookup.
Provides IP geolocation (via ip-api.com with SQLite cache)
and MAC address vendor identification (OUI database).
Background worker thread resolves public IPs without blocking.
"""
import ipaddress
import json
import os
import queue
import sqlite3
import threading
import time
import urllib.request
import urllib.error
# ============================================================
# OUI vendor map — top ~200 manufacturers by IEEE prefix
# ============================================================
OUI_MAP = {
# Apple
"AC:DE:48": "Apple", "3C:22:FB": "Apple", "F0:18:98": "Apple",
"DC:A9:04": "Apple", "A4:83:E7": "Apple", "F4:5C:89": "Apple",
"78:7B:8A": "Apple", "88:66:A5": "Apple", "40:A6:D9": "Apple",
# Samsung
"00:07:AB": "Samsung", "AC:5F:3E": "Samsung", "78:47:1D": "Samsung",
"D0:22:BE": "Samsung", "C4:73:1E": "Samsung", "50:01:BB": "Samsung",
# Espressif (ESP32/ESP8266)
"24:0A:C4": "Espressif", "30:AE:A4": "Espressif", "AC:67:B2": "Espressif",
"24:62:AB": "Espressif", "A4:CF:12": "Espressif", "CC:50:E3": "Espressif",
"84:CC:A8": "Espressif", "10:52:1C": "Espressif", "7C:DF:A1": "Espressif",
"3C:61:05": "Espressif", "EC:FA:BC": "Espressif", "08:3A:F2": "Espressif",
"34:AB:95": "Espressif", "C8:C9:A3": "Espressif", "E8:DB:84": "Espressif",
# Raspberry Pi
"B8:27:EB": "Raspberry Pi", "DC:A6:32": "Raspberry Pi",
"E4:5F:01": "Raspberry Pi", "28:CD:C1": "Raspberry Pi",
"D8:3A:DD": "Raspberry Pi",
# TP-Link
"50:C7:BF": "TP-Link", "14:CF:92": "TP-Link", "E8:DE:27": "TP-Link",
"60:E3:27": "TP-Link", "B0:BE:76": "TP-Link", "A4:2B:B0": "TP-Link",
# Cisco
"00:1A:A1": "Cisco", "00:1B:0D": "Cisco", "00:22:BD": "Cisco",
"00:25:B5": "Cisco", "00:1E:F7": "Cisco", "64:F6:9D": "Cisco",
# Intel
"00:1E:67": "Intel", "3C:97:0E": "Intel", "8C:8D:28": "Intel",
"A4:C3:F0": "Intel", "48:51:B7": "Intel", "80:86:F2": "Intel",
# Realtek
"00:E0:4C": "Realtek", "52:54:00": "Realtek/QEMU",
# Qualcomm / Atheros
"00:03:7F": "Atheros", "1C:B7:2C": "Qualcomm",
# Broadcom
"00:10:18": "Broadcom", "20:10:7A": "Broadcom",
# MediaTek
"00:0C:E7": "MediaTek", "9C:D2:1E": "MediaTek",
# Huawei
"00:E0:FC": "Huawei", "48:46:FB": "Huawei", "88:53:D4": "Huawei",
"70:8A:09": "Huawei", "CC:A2:23": "Huawei",
# Xiaomi
"64:09:80": "Xiaomi", "28:6C:07": "Xiaomi", "78:11:DC": "Xiaomi",
"F8:A4:5F": "Xiaomi", "7C:1C:4E": "Xiaomi",
# Google / Nest
"54:60:09": "Google", "F4:F5:D8": "Google", "A4:77:33": "Google",
# Amazon
"44:65:0D": "Amazon", "68:54:FD": "Amazon", "40:B4:CD": "Amazon",
# Microsoft (Xbox, Surface)
"28:18:78": "Microsoft", "7C:1E:52": "Microsoft",
# Dell
"00:14:22": "Dell", "18:03:73": "Dell", "F8:DB:88": "Dell",
# HP / HPE
"00:1A:4B": "HP", "3C:D9:2B": "HP", "00:1E:0B": "HP",
# Lenovo
"00:06:1B": "Lenovo", "28:D2:44": "Lenovo", "E8:6A:64": "Lenovo",
# Asus
"00:1A:92": "ASUS", "04:D4:C4": "ASUS", "1C:87:2C": "ASUS",
# Netgear
"00:1E:2A": "Netgear", "20:E5:2A": "Netgear", "C4:04:15": "Netgear",
# D-Link
"00:1B:11": "D-Link", "1C:7E:E5": "D-Link", "B0:C5:54": "D-Link",
# Ubiquiti
"04:18:D6": "Ubiquiti", "24:5A:4C": "Ubiquiti", "FC:EC:DA": "Ubiquiti",
"68:D7:9A": "Ubiquiti", "78:8A:20": "Ubiquiti",
# MikroTik
"00:0C:42": "MikroTik", "4C:5E:0C": "MikroTik", "64:D1:54": "MikroTik",
# Synology
"00:11:32": "Synology",
# Hikvision
"C0:56:E3": "Hikvision", "44:47:CC": "Hikvision",
# Dahua
"3C:EF:8C": "Dahua", "A0:BD:1D": "Dahua",
# Aruba / HPE Networking
"00:0B:86": "Aruba", "24:DE:C6": "Aruba",
# VMware
"00:50:56": "VMware", "00:0C:29": "VMware",
# Linux KVM / VirtualBox
"08:00:27": "VirtualBox",
# ZTE
"00:19:CB": "ZTE", "34:4B:50": "ZTE",
# Honeywell
"00:40:84": "Honeywell",
# Siemens
"00:0E:8C": "Siemens",
# ABB
"00:80:F4": "ABB",
# Schneider Electric
"00:80:F4": "Schneider",
# Sonos
"B8:E9:37": "Sonos", "54:2A:1B": "Sonos",
# Sony
"00:1D:BA": "Sony", "AC:9B:0A": "Sony",
# LG
"00:1E:75": "LG", "C4:36:55": "LG",
# OnePlus / Oppo
"94:65:2D": "OnePlus", "A4:3B:FA": "Oppo",
# Ring / Blink
"CC:9E:A2": "Ring/Amazon",
# Tuya IoT
"D8:1F:12": "Tuya",
# Shelly
"E8:68:E7": "Shelly", "EC:FA:BC": "Shelly/Espressif",
# Nordic Semiconductor
"E7:3E:B6": "Nordic Semi",
# Texas Instruments
"00:12:4B": "TI", "D4:F5:13": "TI",
}
class HpGeoLookup:
"""
IP geolocation with SQLite cache + MAC vendor lookup.
Background thread resolves public IPs via ip-api.com.
"""
def __init__(self, db_path=None):
if db_path is None:
db_path = os.path.join(os.path.dirname(__file__), "honeypot_geo.db")
self._db_path = db_path
self._lock = threading.Lock()
self._queue = queue.Queue(maxsize=500)
self._cache = {} # in-memory hot cache: ip -> dict
self._rate_count = 0
self._rate_reset = time.time() + 60
self._init_db()
self._load_cache()
# Background worker thread
self._worker = threading.Thread(target=self._resolver_worker, daemon=True)
self._worker.start()
def _init_db(self):
conn = sqlite3.connect(self._db_path)
conn.execute("""CREATE TABLE IF NOT EXISTS geo_cache (
ip TEXT PRIMARY KEY,
country TEXT, country_code TEXT, city TEXT, isp TEXT,
lat REAL, lon REAL, is_private INTEGER DEFAULT 0,
cached_at REAL
)""")
conn.commit()
conn.close()
def _load_cache(self):
"""Load recent cache entries into memory."""
try:
conn = sqlite3.connect(self._db_path)
conn.row_factory = sqlite3.Row
cutoff = time.time() - 86400 * 7 # 7 days
rows = conn.execute(
"SELECT * FROM geo_cache WHERE cached_at > ?", (cutoff,)
).fetchall()
for r in rows:
self._cache[r["ip"]] = dict(r)
conn.close()
except Exception:
pass
def lookup_ip(self, ip):
"""
Look up IP geolocation. Returns cached result or enqueues
for background resolution. Non-blocking.
"""
if not ip or ip == "0.0.0.0":
return {"country": "", "country_code": "", "is_private": True}
# Check memory cache (with 7-day TTL)
with self._lock:
cached = self._cache.get(ip)
if cached:
if time.time() - cached.get("cached_at", 0) < 86400 * 7:
return cached
del self._cache[ip]
# Private IP — instant result
try:
if ipaddress.ip_address(ip).is_private:
result = {
"ip": ip, "country": "LAN", "country_code": "",
"city": "", "isp": "Private Network",
"lat": 0, "lon": 0, "is_private": 1,
"cached_at": time.time(),
}
with self._lock:
self._cache[ip] = result
self._persist(result)
return result
except ValueError:
return {"country": "", "country_code": "", "is_private": False}
# Enqueue for background resolution
try:
self._queue.put_nowait(ip)
except queue.Full:
pass
return {"country": "...", "country_code": "", "is_private": False,
"pending": True}
def lookup_mac_vendor(self, mac):
"""Instant MAC vendor lookup from OUI map."""
if not mac or mac == "00:00:00:00:00:00":
return ""
prefix = mac[:8].upper()
return OUI_MAP.get(prefix, "")
def enrich_attacker(self, attacker):
"""Add geo and vendor info to an attacker dict."""
geo = self.lookup_ip(attacker.get("ip", ""))
attacker["country"] = geo.get("country", "")
attacker["country_code"] = geo.get("country_code", "")
attacker["city"] = geo.get("city", "")
attacker["isp"] = geo.get("isp", "")
attacker["is_private"] = geo.get("is_private", False)
attacker["vendor"] = self.lookup_mac_vendor(attacker.get("mac", ""))
return attacker
def _persist(self, result):
"""Write geo result to SQLite cache."""
try:
conn = sqlite3.connect(self._db_path)
conn.execute(
"""INSERT OR REPLACE INTO geo_cache
(ip, country, country_code, city, isp, lat, lon,
is_private, cached_at)
VALUES (?,?,?,?,?,?,?,?,?)""",
(result["ip"], result.get("country", ""),
result.get("country_code", ""), result.get("city", ""),
result.get("isp", ""), result.get("lat", 0),
result.get("lon", 0), result.get("is_private", 0),
result.get("cached_at", time.time()))
)
conn.commit()
conn.close()
except Exception:
pass
def _resolver_worker(self):
"""Background thread: resolve IPs from queue via ip-api.com."""
while True:
try:
ip = self._queue.get(timeout=5)
except queue.Empty:
continue
# Already cached?
with self._lock:
if ip in self._cache:
continue
# Rate limit: 45 requests per minute
now = time.time()
if now > self._rate_reset:
self._rate_count = 0
self._rate_reset = now + 60
if self._rate_count >= 45:
# Wait until reset
time.sleep(max(0, self._rate_reset - now))
self._rate_count = 0
self._rate_reset = time.time() + 60
try:
url = f"http://ip-api.com/json/{ip}?fields=status,country,countryCode,city,isp,lat,lon"
req = urllib.request.Request(url, headers={"User-Agent": "Espilon-HP/1.0"})
resp = urllib.request.urlopen(req, timeout=5)
data = json.loads(resp.read().decode())
self._rate_count += 1
if data.get("status") == "success":
result = {
"ip": ip,
"country": data.get("country", ""),
"country_code": data.get("countryCode", ""),
"city": data.get("city", ""),
"isp": data.get("isp", ""),
"lat": data.get("lat", 0),
"lon": data.get("lon", 0),
"is_private": 0,
"cached_at": time.time(),
}
with self._lock:
# Cap cache at 50,000 entries
if len(self._cache) >= 50000:
oldest = min(self._cache,
key=lambda k: self._cache[k].get("cached_at", 0))
del self._cache[oldest]
self._cache[ip] = result
self._persist(result)
except Exception:
pass