Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c020bd318a | ||
|
|
ff18511ff2 | ||
|
|
bcb8f9532f | ||
|
|
e8c6f98182 |
BIN
Binary file not shown.
@@ -19,3 +19,6 @@ package-dir = {"" = "src"}
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["src"]
|
||||
|
||||
@@ -135,7 +135,8 @@ def command_for(operation: str, args: list[str]) -> list[str]:
|
||||
target = validate_target(args[0])
|
||||
return [
|
||||
nmap(), "-sS", "-sV", "-O", "--osscan-guess", "--version-light",
|
||||
"--open", "-T4", "--top-ports", "100", "-oX", "-", target,
|
||||
"--open", "-T4", "--max-retries", "1", "--host-timeout", "60s",
|
||||
"--top-ports", "100", "-oX", "-", target,
|
||||
]
|
||||
|
||||
if operation == "nmap-deep-hosts":
|
||||
@@ -145,7 +146,8 @@ def command_for(operation: str, args: list[str]) -> list[str]:
|
||||
hosts = validate_host_list(args)
|
||||
return [
|
||||
nmap(), "-Pn", "-n", "-sS", "-sV", "-O", "--osscan-guess",
|
||||
"--version-light", "--open", "-T4", "--top-ports", "1000",
|
||||
"--version-light", "--open", "-T4", "--max-retries", "1",
|
||||
"--host-timeout", "60s", "--top-ports", "1000",
|
||||
"-oX", "-", *hosts,
|
||||
]
|
||||
|
||||
@@ -155,7 +157,8 @@ def command_for(operation: str, args: list[str]) -> list[str]:
|
||||
host = validate_target(args[0], network_allowed=False)
|
||||
return [
|
||||
nmap(), "-Pn", "-n", "-sS", "-sV", "-O", "--osscan-guess", "--version-light",
|
||||
"--open", "-T4", "--top-ports", "1000", "-oX", "-", host,
|
||||
"--open", "-T4", "--max-retries", "1", "--host-timeout", "60s",
|
||||
"--top-ports", "1000", "-oX", "-", host,
|
||||
]
|
||||
|
||||
raise ValueError("Opération privilégiée non autorisée")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -19,14 +18,29 @@ class PrivilegeDiagnostic:
|
||||
|
||||
|
||||
def find_pkexec() -> str | None:
|
||||
return shutil.which("pkexec") or ("/usr/bin/pkexec" if os.path.isfile("/usr/bin/pkexec") else None)
|
||||
path = "/usr/bin/pkexec"
|
||||
try:
|
||||
info = os.stat(path)
|
||||
except OSError:
|
||||
return None
|
||||
if os.path.isfile(path) and os.access(path, os.X_OK) and info.st_uid == 0 and not (info.st_mode & 0o022):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def find_helper() -> str | None:
|
||||
override = os.environ.get("LIBRENET_PRIVILEGED_HELPER", "").strip()
|
||||
if override and os.path.isfile(override) and os.access(override, os.X_OK):
|
||||
return override
|
||||
if os.path.isfile(HELPER_PATH) and os.access(HELPER_PATH, os.X_OK):
|
||||
# Le chemin du helper est une frontière de privilèges : ne pas permettre à
|
||||
# l'environnement de l'utilisateur de le remplacer par un exécutable arbitraire.
|
||||
try:
|
||||
info = os.stat(HELPER_PATH)
|
||||
except OSError:
|
||||
return None
|
||||
if (
|
||||
os.path.isfile(HELPER_PATH)
|
||||
and os.access(HELPER_PATH, os.X_OK)
|
||||
and info.st_uid == 0
|
||||
and not (info.st_mode & 0o022)
|
||||
):
|
||||
return HELPER_PATH
|
||||
return None
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ NAABU_BATCH_TIMEOUT_SECONDS = 10.0
|
||||
NAABU_CONNECT_TIMEOUT = "800ms"
|
||||
NAABU_RATE = "2500"
|
||||
NAABU_CONCURRENCY = "100"
|
||||
RAPID_DISCOVERY_MAX_SECONDS = 45.0
|
||||
DEEP_SCAN_MAX_SECONDS = 180.0
|
||||
HOST_SCAN_MAX_SECONDS = 60.0
|
||||
|
||||
COMMON_PORTS = (
|
||||
"21,22,23,25,53,80,110,135,139,143,389,443,445,465,515,587,631,636,993,995,"
|
||||
@@ -589,7 +592,7 @@ class ScanWorker(QThread):
|
||||
if not ips:
|
||||
return []
|
||||
prefix = "Base Approfondi" if self.request.profile == "Approfondi" else "Standard"
|
||||
suffix = " — REPLI Naabu" if fallback else " — moteur adaptatif"
|
||||
suffix = " — REPLI Nmap" if fallback else " — moteur adaptatif"
|
||||
if self.request.privileged:
|
||||
args = privileged_command("nmap-standard", *ips)
|
||||
label = f"{prefix} — ports Nmap SYN (Admin){suffix}…"
|
||||
@@ -753,14 +756,13 @@ class ScanWorker(QThread):
|
||||
def _discover_hosts(self, *, start_percent: int = 4, end_percent: int = 52) -> set[str]:
|
||||
"""Découverte robuste et additive des hôtes.
|
||||
|
||||
IMPORTANT : le mode administrateur ne remplace plus la découverte normale.
|
||||
Il ajoute ARP/Nmap privilégiés aux résultats non privilégiés. Ainsi activer
|
||||
les privilèges ne peut pas réduire le nombre d'hôtes détectés.
|
||||
IMPORTANT : le mode administrateur ne remplace pas la découverte normale.
|
||||
Il ajoute les informations ARP privilégiées aux résultats Nmap utilisateur,
|
||||
sans relancer une seconde découverte complète.
|
||||
"""
|
||||
span = max(20, end_percent - start_percent)
|
||||
arp_end = start_percent + round(span * 0.20)
|
||||
normal_end = start_percent + round(span * 0.62)
|
||||
admin_end = start_percent + round(span * 0.84)
|
||||
neighbor_percent = start_percent + round(span * 0.94)
|
||||
|
||||
local_hosts = self._emit_local_host()
|
||||
@@ -777,34 +779,24 @@ class ScanWorker(QThread):
|
||||
"Découverte Nmap utilisateur échouée",
|
||||
start_percent=arp_end,
|
||||
end_percent=normal_end,
|
||||
timeout_seconds=min(
|
||||
RAPID_DISCOVERY_MAX_SECONDS,
|
||||
max(12.0, 8.0 + len(target_ipv4_hosts(self.request.target)) / 32.0),
|
||||
),
|
||||
)
|
||||
|
||||
if self.isInterruptionRequested():
|
||||
return union_host_ips(local_hosts, arp_hosts, normal_hosts)
|
||||
|
||||
privileged_hosts: list[Host] = []
|
||||
if self.request.privileged and not self.isInterruptionRequested():
|
||||
try:
|
||||
cmd = privileged_command("nmap-discover", self.request.target)
|
||||
except RuntimeError as exc:
|
||||
self.warning.emit(str(exc))
|
||||
else:
|
||||
privileged_hosts = self._nmap_optional(
|
||||
cmd,
|
||||
"Découverte des hôtes — Nmap (Admin complémentaire)…",
|
||||
"Découverte Nmap Admin complémentaire échouée",
|
||||
start_percent=normal_end,
|
||||
end_percent=admin_end,
|
||||
)
|
||||
|
||||
known_ips = union_host_ips(arp_hosts, normal_hosts, privileged_hosts)
|
||||
# Le mode Admin enrichit la découverte locale via ARP, mais ne relance pas
|
||||
# une seconde découverte Nmap identique. Les privilèges sont réservés aux
|
||||
# opérations qui en ont réellement besoin (SYN, OS et ARP).
|
||||
known_ips = union_host_ips(arp_hosts, normal_hosts)
|
||||
known_ips.update(union_host_ips(local_hosts))
|
||||
if self.isInterruptionRequested():
|
||||
return known_ips
|
||||
self._neighbor_hosts(known_ips, percent=neighbor_percent)
|
||||
details = f"local {len(local_hosts)} · ARP {len(arp_hosts)} · Nmap {len(normal_hosts)}"
|
||||
if self.request.privileged:
|
||||
details += f" · admin {len(privileged_hosts)}"
|
||||
self._set_progress(end_percent, f"Découverte : {len(known_ips)} hôte(s) unique(s) — {details}")
|
||||
return known_ips
|
||||
|
||||
@@ -838,12 +830,18 @@ class ScanWorker(QThread):
|
||||
else:
|
||||
deep_args = [
|
||||
"nmap", "-Pn", "-n", "-sT", "-sV", "--version-light",
|
||||
"--open", "-T4", "--top-ports", "1000",
|
||||
"--open", "-T4", "--max-retries", "1", "--host-timeout", "60s",
|
||||
"--top-ports", "1000",
|
||||
"-oX", "-", *ips,
|
||||
]
|
||||
deep_label = "Enrichissement approfondi — Nmap TCP et services…"
|
||||
deep_timeout = min(
|
||||
DEEP_SCAN_MAX_SECONDS,
|
||||
max(90.0, 30.0 + len(ips) * 1.5),
|
||||
)
|
||||
deep_hosts = self._nmap(
|
||||
deep_args, deep_label, start_percent=62, end_percent=96
|
||||
deep_args, deep_label, start_percent=62, end_percent=96,
|
||||
timeout_seconds=deep_timeout,
|
||||
)
|
||||
if deep_hosts:
|
||||
self.hosts_found.emit(deep_hosts)
|
||||
@@ -927,6 +925,7 @@ class HostScanWorker(QThread):
|
||||
else:
|
||||
args = [
|
||||
"nmap", "-Pn", "-n", "-sT", "-sV", "--version-light", "--open", "-T4",
|
||||
"--max-retries", "1", "--host-timeout", "60s",
|
||||
"--top-ports", "1000", "-oX", "-", self.ip,
|
||||
]
|
||||
try:
|
||||
@@ -961,10 +960,16 @@ class HostScanWorker(QThread):
|
||||
for reader in readers:
|
||||
reader.start()
|
||||
stop_sent = False
|
||||
started = time.monotonic()
|
||||
timed_out = False
|
||||
while self._proc.poll() is None:
|
||||
if self.isInterruptionRequested() and not stop_sent:
|
||||
self.stop()
|
||||
stop_sent = True
|
||||
elif not stop_sent and time.monotonic() - started >= HOST_SCAN_MAX_SECONDS:
|
||||
timed_out = True
|
||||
self.stop()
|
||||
stop_sent = True
|
||||
try:
|
||||
self._proc.wait(timeout=0.10)
|
||||
except subprocess.TimeoutExpired:
|
||||
@@ -973,7 +978,26 @@ class HostScanWorker(QThread):
|
||||
reader.join(timeout=1.0)
|
||||
stdout, stderr = "".join(stdout_parts), "".join(stderr_parts)
|
||||
else:
|
||||
stdout, stderr = self._proc.communicate()
|
||||
timed_out = False
|
||||
try:
|
||||
stdout, stderr = self._proc.communicate(timeout=HOST_SCAN_MAX_SECONDS)
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
try:
|
||||
os.killpg(self._proc.pid, signal.SIGTERM)
|
||||
except (ProcessLookupError, PermissionError, OSError):
|
||||
pass
|
||||
try:
|
||||
stdout, stderr = self._proc.communicate(timeout=1.5)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
os.killpg(self._proc.pid, signal.SIGKILL)
|
||||
except (ProcessLookupError, PermissionError, OSError):
|
||||
pass
|
||||
stdout, stderr = self._proc.communicate()
|
||||
if timed_out:
|
||||
self.failed.emit(f"Délai maximal dépassé ({HOST_SCAN_MAX_SECONDS:.0f} s).")
|
||||
return
|
||||
if self.isInterruptionRequested():
|
||||
return
|
||||
if self._proc.returncode != 0:
|
||||
|
||||
@@ -136,8 +136,9 @@ class PrivilegeAuthWorker(QThread):
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
check=False,
|
||||
timeout=30.0,
|
||||
)
|
||||
except (OSError, RuntimeError) as exc:
|
||||
except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc:
|
||||
self.result.emit(False, str(exc))
|
||||
return
|
||||
if proc.returncode == 0:
|
||||
@@ -1039,6 +1040,15 @@ class MainWindow(QMainWindow):
|
||||
self.statusBar().showMessage("Disposition réinitialisée", 3000)
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
# Une fenêtre Qt ne doit pas détruire un QThread encore actif. Les workers
|
||||
# arrêtent aussi leur groupe de processus, y compris le helper pkexec.
|
||||
self.stop_scan()
|
||||
workers = [self.worker, self.host_worker, self.auth_worker, self.vendor_lookup_worker]
|
||||
for worker in workers:
|
||||
if worker and worker.isRunning() and not worker.wait(3500):
|
||||
self.statusBar().showMessage("Arrêt du scan encore en cours…", 5000)
|
||||
event.ignore()
|
||||
return
|
||||
self._save_ui_layout()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ class EngineStatus0419Tests(unittest.TestCase):
|
||||
self.assertIn("découverte rapide Nmap/ARP", source)
|
||||
self.assertIn("ports Naabu SYN (Admin)", source)
|
||||
self.assertIn("ports Nmap SYN (Admin)", source)
|
||||
self.assertIn("REPLI Naabu", source)
|
||||
self.assertIn("REPLI Nmap", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -112,6 +112,7 @@ class StandardBehavior0421Tests(unittest.TestCase):
|
||||
args = worker._nmap.call_args.args[0]
|
||||
self.assertIn("-sV", args)
|
||||
self.assertIn("1000", args)
|
||||
self.assertGreaterEqual(worker._nmap.call_args.kwargs["timeout_seconds"], 90.0)
|
||||
|
||||
|
||||
class Diagnostics0421Tests(unittest.TestCase):
|
||||
|
||||
@@ -138,6 +138,30 @@ class PerformancePipeline0423Tests(unittest.TestCase):
|
||||
self.assertEqual(cmd[:6], ["nmap", "-sn", "-n", "-T4", "--max-retries", "1"])
|
||||
self.assertIsNotNone(worker._nmap.call_args.kwargs["timeout_seconds"])
|
||||
|
||||
def test_rapid_discovery_has_a_wall_clock_timeout(self):
|
||||
worker = self.worker()
|
||||
worker.request.profile = "Rapide"
|
||||
worker._emit_local_host = Mock(return_value=[])
|
||||
worker._emit_arp = Mock(return_value=[])
|
||||
worker._neighbor_hosts = Mock(return_value=[])
|
||||
worker._set_progress = Mock()
|
||||
worker._nmap_optional = Mock(return_value=[])
|
||||
worker._discover_hosts(start_percent=4, end_percent=96)
|
||||
timeout = worker._nmap_optional.call_args.kwargs["timeout_seconds"]
|
||||
self.assertIsNotNone(timeout)
|
||||
self.assertLessEqual(timeout, scanner.RAPID_DISCOVERY_MAX_SECONDS)
|
||||
|
||||
def test_admin_rapid_discovery_does_not_repeat_nmap(self):
|
||||
worker = self.worker(privileged=True)
|
||||
worker.request.profile = "Rapide"
|
||||
worker._emit_local_host = Mock(return_value=[])
|
||||
worker._emit_arp = Mock(return_value=[])
|
||||
worker._neighbor_hosts = Mock(return_value=[])
|
||||
worker._set_progress = Mock()
|
||||
worker._nmap_optional = Mock(return_value=[])
|
||||
worker._discover_hosts(start_percent=4, end_percent=96)
|
||||
worker._nmap_optional.assert_called_once()
|
||||
|
||||
def test_naabu_failure_does_not_rescan_dead_addresses_with_nmap(self):
|
||||
worker = self.worker()
|
||||
live = {f"192.168.10.{i}" for i in range(1, scanner.NAABU_ACTIVE_HOST_THRESHOLD + 1)}
|
||||
|
||||
Reference in New Issue
Block a user