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()