175 lines
6.2 KiB
Python
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()
|