Последняя версия сканера устройств (хранение истории, вывод портов и вывод последнего входа)
This commit is contained in:
parent
d67ea17fd7
commit
6b6239ef9d
1 changed files with 55 additions and 82 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue