Files
librenet-scanner/src/librenet_scanner/scanner.py
T
2026-08-21 15:21:29 +02:00

175 lines
6.2 KiB
Python

from __future__ import annotations
import ipaddress
import os
import shutil
import subprocess
from dataclasses import dataclass
from PySide6.QtCore import QThread, Signal
from .models import Host
from .network import NetworkInterface, target_is_on_interface
from .parsers import parse_arp_scan, parse_nmap_xml
COMMON_PORTS = "22,23,53,80,139,443,445,3389,5900,8006,8080,8443,9100"
@dataclass(slots=True)
class ScanRequest:
target: str
profile: str
interface: NetworkInterface | None = None
class ScanWorker(QThread):
progress = Signal(str)
hosts_found = Signal(object)
failed = Signal(str)
completed = Signal()
def __init__(self, request: ScanRequest, parent=None) -> None:
super().__init__(parent)
self.request = request
self._proc: subprocess.Popen[str] | None = None
def stop(self) -> None:
self.requestInterruption()
proc = self._proc
if proc and proc.poll() is None:
proc.terminate()
def _run_command(self, args: list[str], label: str) -> tuple[int, str, str]:
if self.isInterruptionRequested():
return 130, "", "Interrompu"
self.progress.emit(label)
try:
self._proc = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
stdout, stderr = self._proc.communicate()
return self._proc.returncode or 0, stdout, stderr
except FileNotFoundError:
return 127, "", f"Commande introuvable : {args[0]}"
except OSError as exc:
return 1, "", str(exc)
finally:
self._proc = None
def _arp_scan_command(self) -> str | None:
found = shutil.which("arp-scan")
if found:
return found
for candidate in ("/usr/sbin/arp-scan", "/usr/bin/arp-scan"):
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
return None
def _emit_arp(self) -> bool:
command = self._arp_scan_command()
if not command:
return False
iface = self.request.interface
if not iface or not target_is_on_interface(self.request.target, iface):
return False
args = [command, "--interface", iface.name, self.request.target]
code, stdout, stderr = self._run_command(args, "Découverte ARP…")
# arp-scan peut retourner 1 si aucun hôte n'est trouvé : on ne bloque pas le scan Nmap.
hosts = parse_arp_scan(stdout)
if hosts:
self.hosts_found.emit(hosts)
if code not in (0, 1) and stderr:
self.progress.emit(f"arp-scan indisponible sans privilèges, repli Nmap : {stderr.strip().splitlines()[-1]}")
return bool(hosts)
def _nmap(self, args: list[str], label: str) -> list[Host]:
code, stdout, stderr = self._run_command(args, label)
if self.isInterruptionRequested():
return []
if code != 0:
raise RuntimeError(stderr.strip() or f"Nmap a quitté avec le code {code}")
return parse_nmap_xml(stdout)
def run(self) -> None:
try:
profile = self.request.profile
target = self.request.target
if profile == "Rapide":
arp_ok = self._emit_arp()
if not arp_ok:
hosts = self._nmap(["nmap", "-sn", "-oX", "-", target], "Découverte Nmap…")
if hosts:
self.hosts_found.emit(hosts)
elif profile == "Standard":
self._emit_arp()
hosts = self._nmap(["nmap", "-sn", "-oX", "-", target], "Découverte des hôtes…")
if hosts:
self.hosts_found.emit(hosts)
if self.isInterruptionRequested():
return
ips = [h.ip for h in hosts]
if ips:
args = ["nmap", "-Pn", "-sT", "--open", "-T4", "-p", COMMON_PORTS, "-oX", "-", *ips]
port_hosts = self._nmap(args, "Scan des ports usuels…")
if port_hosts:
self.hosts_found.emit(port_hosts)
elif profile == "Approfondi":
self._emit_arp()
args = ["nmap", "-sT", "-sV", "--version-light", "--open", "-T4", "--top-ports", "100", "-oX", "-", target]
hosts = self._nmap(args, "Scan approfondi : services et 100 ports principaux…")
if hosts:
self.hosts_found.emit(hosts)
else:
raise RuntimeError(f"Profil inconnu : {profile}")
except Exception as exc: # frontière thread -> GUI
self.failed.emit(str(exc))
finally:
self.completed.emit()
class HostScanWorker(QThread):
progress = Signal(str)
result = Signal(object)
failed = Signal(str)
completed = Signal()
def __init__(self, ip: str, parent=None) -> None:
super().__init__(parent)
self.ip = str(ipaddress.ip_address(ip))
self._proc: subprocess.Popen[str] | None = None
def stop(self) -> None:
self.requestInterruption()
if self._proc and self._proc.poll() is None:
self._proc.terminate()
def run(self) -> None:
self.progress.emit(f"Scan détaillé de {self.ip}…")
args = ["nmap", "-sT", "-sV", "--version-light", "--open", "-T4", "--top-ports", "1000", "-oX", "-", self.ip]
try:
self._proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8", errors="replace")
stdout, stderr = self._proc.communicate()
if self.isInterruptionRequested():
return
if self._proc.returncode != 0:
self.failed.emit(stderr.strip() or f"Nmap a quitté avec le code {self._proc.returncode}")
return
hosts = parse_nmap_xml(stdout)
if hosts:
self.result.emit(hosts[0])
except OSError as exc:
self.failed.emit(str(exc))
finally:
self._proc = None
self.completed.emit()