Последняя версия сканера устройств (хранение истории, вывод портов и вывод последнего входа)

This commit is contained in:
parent d67ea17fd7
commit 6b6239ef9d

View file

@ -1,5 +1,5 @@
from scapy.config import conf
from manuf import MacParser
from mac_vendor_lookup import MacLookup
from typing import List, Optional, Tuple, Dict
import socket
import logging
@ -12,7 +12,6 @@ import json
from pathlib import Path
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import os
from models.subnet import SubnetScanResult, Device
from config.ports import port_config
@ -23,17 +22,6 @@ from scapy.sendrecv import srp, sr1
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
# Подавляем логи Scapy
logging.getLogger("scapy").setLevel(logging.ERROR)
def normalize_mac(mac: str) -> str:
"""Нормализует MAC-адрес к формату XX:XX:XX:XX:XX:XX."""
if not mac:
return ""
mac = mac.replace(":", "").replace("-", "").upper()
return ":".join(mac[i:i + 2] for i in range(0, 12, 2))
class NetworkScanner:
"""Класс для сканирования сети с помощью ARP-запросов."""
@ -41,19 +29,14 @@ class NetworkScanner:
def __init__(self, interface: str):
self.interface = interface
conf.iface = interface
self.arp_cache: Dict[str, str] = {}
def scan(self, subnet: str) -> List[Tuple[str, str]]:
try:
arp = ARP(pdst=subnet)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether / arp
result = srp(packet, timeout=5, verbose=False, iface=self.interface)[0]
devices = [(received.psrc, normalize_mac(received.hwsrc)) for sent, received in result]
for ip, mac in devices:
self.arp_cache[ip] = mac
logger.debug(f"ARP-кэш обновлён: {len(self.arp_cache)} записей")
return devices
result = srp(packet, timeout=2, verbose=False, iface=self.interface)[0]
return [(received.psrc, received.hwsrc) for sent, received in result]
except Exception as e:
logger.error(f"Ошибка ARP-сканирования подсети {subnet}: {str(e)}")
return []
@ -62,18 +45,16 @@ class NetworkScanner:
class PortScanner:
"""Класс для сканирования портов на устройстве."""
def __init__(self, interface: str, arp_cache: Dict[str, str]):
def __init__(self, interface: str):
self.interface = interface
self.arp_cache = arp_cache
def scan(self, ip: str) -> List[str]:
open_ports = []
def check_port(port: int) -> Optional[str]:
try:
dst_mac = self.arp_cache.get(ip, "ff:ff:ff:ff:ff:ff")
pkt = sr1(
Ether(dst=dst_mac) / IP(dst=ip) / TCP(dport=port, flags="S"),
IP(dst=ip) / TCP(dport=port, flags="S"),
timeout=2,
verbose=False,
iface=self.interface
@ -86,7 +67,7 @@ class PortScanner:
return None
try:
max_workers = min(len(port_config.get_ports()), 20) # Уменьшено до 20
max_workers = min(len(port_config.get_ports()), 15) # Снижено с 100
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = executor.map(check_port, port_config.get_ports())
open_ports = [port for port in results if port is not None]
@ -101,33 +82,14 @@ class DeviceInfoCollector:
"""Класс для сбора информации об устройстве."""
def __init__(self):
self.mac_lookup = MacParser()
try:
logger.info("Обновление базы производителей MAC...")
self.mac_lookup.update()
logger.info("База производителей обновлена")
except Exception as e:
logger.warning(f"Не удалось обновить базу производителей: {str(e)}")
# Проверяем наличие локальной базы
manuf_db = Path(os.path.expanduser("~/.cache/manuf/manuf.txt"))
if manuf_db.exists():
logger.info(f"Используется локальная база OUI: {manuf_db}")
else:
logger.error("Локальная база OUI отсутствует, vendors могут быть Unknown")
self.mac_lookup = MacLookup()
@lru_cache(maxsize=1000)
@lru_cache(maxsize=1000) # Снижено с 10000
def get_vendor(self, mac: str) -> str:
if not mac or mac.lower() == "ff:ff:ff:ff:ff:ff":
return "Unknown"
normalized_mac = normalize_mac(mac)
try:
vendor = self.mac_lookup.get_manuf(normalized_mac)
if vendor is None:
vendor = "Unknown"
logger.debug(f"Vendor для MAC {normalized_mac}: {vendor}")
return vendor
return self.mac_lookup.lookup(mac)
except Exception as e:
logger.debug(f"Не удалось определить производителя для MAC {normalized_mac}: {str(e)}")
logger.warning(f"Не удалось определить производителя для MAC {mac}: {str(e)}")
return "Unknown"
def get_hostname(self, ip: str) -> Optional[str]:
@ -154,7 +116,7 @@ class DeviceStorage:
for device_data in data.get("devices", []):
device = Device(**device_data)
if device.mac:
devices[normalize_mac(device.mac)] = device
devices[device.mac] = device
return self._clean_old_devices(devices)
return {}
except Exception as e:
@ -180,11 +142,6 @@ class DeviceStorage:
def save_devices(self, devices: List[Device]) -> None:
try:
for device in devices:
if isinstance(device.last_seen, datetime):
device.last_seen = device.last_seen.isoformat()
if device.mac:
device.mac = normalize_mac(device.mac)
data = SubnetScanResult(devices=devices).model_dump()
with open(self.storage_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
@ -199,18 +156,18 @@ class SubnetService:
def __init__(self):
self.interface = self._get_default_interface()
self.network_scanner = NetworkScanner(self.interface)
self.port_scanner = PortScanner(self.interface)
self.device_info = DeviceInfoCollector()
self.port_scanner = PortScanner(self.interface, self.network_scanner.arp_cache)
self.storage = DeviceStorage()
def _get_default_interface(self):
"""Определяет основной сетевой интерфейс."""
def _get_default_interface(self) -> str:
try:
gateways = netifaces.gateways()
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
if default_gateway:
return default_gateway[1]
for iface in netifaces.interfaces():
interfaces = netifaces.interfaces()
for iface in interfaces:
if iface != "lo":
return iface
return "eth0"
@ -219,63 +176,79 @@ class SubnetService:
return "eth0"
def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult:
"""Сканирование подсети и результаты."""
start_time = time.time()
devices = []
now = datetime.now(ZoneInfo("UTC")).isoformat()
try:
# Загружаем старую карту
saved_devices = self.storage.load_devices()
# Получаем локальное устройство
local_device = self._get_local_device(subnet)
if local_device:
local_device.status = "online"
local_device.last_seen = now
devices.append(local_device)
if local_device.mac:
saved_devices[normalize_mac(local_device.mac)] = local_device
saved_devices[local_device.mac] = local_device
# Сканируем подсеть
discovered_devices = self.network_scanner.scan(subnet)
with ThreadPoolExecutor(max_workers=20) as executor: # Уменьшено до 20
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
ip, mac = ip_mac
mac = normalize_mac(mac)
if local_device and ip == local_device.ip:
return None
existing_device = saved_devices.get(mac)
vendor = self.device_info.get_vendor(mac)
if vendor == "Unknown" and existing_device and existing_device.vendor != "Unknown":
vendor = existing_device.vendor
logger.debug(f"Использован сохранённый vendor для MAC {mac}: {vendor}")
# Обрабатываем обнаруженные устройства
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
ip, mac = ip_mac
if local_device and ip == local_device.ip:
return None
# Проверяем, есть ли устройство в старой карте
existing_device = saved_devices.get(mac)
if existing_device:
# Обновляем данные, если MAC совпадает
return Device(
ip=ip,
mac=mac,
hostname=self.device_info.get_hostname(ip) or (
existing_device.hostname if existing_device else None),
vendor=vendor,
open_ports=self.port_scanner.scan(ip) or [],
hostname=self.device_info.get_hostname(ip) or existing_device.hostname,
vendor=self.device_info.get_vendor(mac),
open_ports=self.port_scanner.scan(ip) or existing_device.open_ports,
status="online",
last_seen=now
)
# Новое устройство
vendor = self.device_info.get_vendor(mac)
hostname = self.device_info.get_hostname(ip)
open_ports = self.port_scanner.scan(ip)
return Device(
ip=ip,
mac=mac,
hostname=hostname,
vendor=vendor,
open_ports=open_ports,
status="online",
last_seen=now
)
with ThreadPoolExecutor(max_workers=10) as executor:
device_results = executor.map(process_device, discovered_devices)
devices.extend([d for d in device_results if d is not None])
# Добавляем оффлайн-устройства из старой карты
active_macs = {d.mac for d in devices if d.mac}
for mac, device in saved_devices.items():
if mac not in active_macs:
device.status = "offline"
devices.append(device)
devices.sort(key=lambda x: int(ipaddress.ip_address(x.ip)))
# Сортировка по IP
devices.sort(key=lambda x: int(ipaddress.IPv4Address(x.ip)))
# Сохраняем обновлённую карту
for device in devices:
if device.mac:
saved_devices[device.mac] = device
self.storage.save_devices(devices)
self.storage.save_devices(list(saved_devices.values()))
logger.info(
f"Сканирование подсети {subnet} завершено за {time.time() - start_time:.2f} сек, "
f"Сканирование подсети {subnet} завершено за {time.time() - start_time:.2f} секунд, "
f"найдено {len(devices)} устройств (активных: {len(active_macs)})"
)
return SubnetScanResult(devices=devices)
@ -285,22 +258,22 @@ class SubnetService:
return SubnetScanResult(devices=devices)
def _get_local_device(self, subnet: str) -> Optional[Device]:
"""Получает информацию о локальном устройстве."""
now = datetime.now(ZoneInfo("UTC")).isoformat()
try:
subnet_network = ipaddress.ip_network(subnet, strict=False)
for iface in netifaces.interfaces():
interfaces = netifaces.interfaces()
for iface in interfaces:
if iface == "lo":
continue
addrs = netifaces.ifaddresses(iface)
if netifaces.AF_INET in addrs and netifaces.AF_LINK in addrs:
ip = addrs[netifaces.AF_INET][0]["addr"]
if ipaddress.ip_address(ip) in subnet_network:
mac = normalize_mac(addrs[netifaces.AF_LINK][0]["addr"])
mac = addrs[netifaces.AF_LINK][0]["addr"]
vendor = self.device_info.get_vendor(mac)
hostname = self.device_info.get_hostname(ip)
open_ports = self.port_scanner.scan(ip)
logger.debug(f"Локальное устройство: IP={ip}, MAC={mac}, Vendor={vendor}, Ports={open_ports}")
logger.debug(f"Локальное устройство: IP={ip}, MAC={mac}, Ports={open_ports}")
return Device(
ip=ip,
mac=mac,