first commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user