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.
313 lines
11 KiB
Python
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
|