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]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
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])
|
target = validate_target(args[0])
|
||||||
return [
|
return [
|
||||||
nmap(), "-sS", "-sV", "-O", "--osscan-guess", "--version-light",
|
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":
|
if operation == "nmap-deep-hosts":
|
||||||
@@ -145,7 +146,8 @@ def command_for(operation: str, args: list[str]) -> list[str]:
|
|||||||
hosts = validate_host_list(args)
|
hosts = validate_host_list(args)
|
||||||
return [
|
return [
|
||||||
nmap(), "-Pn", "-n", "-sS", "-sV", "-O", "--osscan-guess",
|
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,
|
"-oX", "-", *hosts,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -155,7 +157,8 @@ def command_for(operation: str, args: list[str]) -> list[str]:
|
|||||||
host = validate_target(args[0], network_allowed=False)
|
host = validate_target(args[0], network_allowed=False)
|
||||||
return [
|
return [
|
||||||
nmap(), "-Pn", "-n", "-sS", "-sV", "-O", "--osscan-guess", "--version-light",
|
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")
|
raise ValueError("Opération privilégiée non autorisée")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@@ -19,14 +18,29 @@ class PrivilegeDiagnostic:
|
|||||||
|
|
||||||
|
|
||||||
def find_pkexec() -> str | None:
|
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:
|
def find_helper() -> str | None:
|
||||||
override = os.environ.get("LIBRENET_PRIVILEGED_HELPER", "").strip()
|
# Le chemin du helper est une frontière de privilèges : ne pas permettre à
|
||||||
if override and os.path.isfile(override) and os.access(override, os.X_OK):
|
# l'environnement de l'utilisateur de le remplacer par un exécutable arbitraire.
|
||||||
return override
|
try:
|
||||||
if os.path.isfile(HELPER_PATH) and os.access(HELPER_PATH, os.X_OK):
|
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 HELPER_PATH
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ NAABU_BATCH_TIMEOUT_SECONDS = 10.0
|
|||||||
NAABU_CONNECT_TIMEOUT = "800ms"
|
NAABU_CONNECT_TIMEOUT = "800ms"
|
||||||
NAABU_RATE = "2500"
|
NAABU_RATE = "2500"
|
||||||
NAABU_CONCURRENCY = "100"
|
NAABU_CONCURRENCY = "100"
|
||||||
|
RAPID_DISCOVERY_MAX_SECONDS = 45.0
|
||||||
|
DEEP_SCAN_MAX_SECONDS = 180.0
|
||||||
|
HOST_SCAN_MAX_SECONDS = 60.0
|
||||||
|
|
||||||
COMMON_PORTS = (
|
COMMON_PORTS = (
|
||||||
"21,22,23,25,53,80,110,135,139,143,389,443,445,465,515,587,631,636,993,995,"
|
"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:
|
if not ips:
|
||||||
return []
|
return []
|
||||||
prefix = "Base Approfondi" if self.request.profile == "Approfondi" else "Standard"
|
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:
|
if self.request.privileged:
|
||||||
args = privileged_command("nmap-standard", *ips)
|
args = privileged_command("nmap-standard", *ips)
|
||||||
label = f"{prefix} — ports Nmap SYN (Admin){suffix}…"
|
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]:
|
def _discover_hosts(self, *, start_percent: int = 4, end_percent: int = 52) -> set[str]:
|
||||||
"""Découverte robuste et additive des hôtes.
|
"""Découverte robuste et additive des hôtes.
|
||||||
|
|
||||||
IMPORTANT : le mode administrateur ne remplace plus la découverte normale.
|
IMPORTANT : le mode administrateur ne remplace pas la découverte normale.
|
||||||
Il ajoute ARP/Nmap privilégiés aux résultats non privilégiés. Ainsi activer
|
Il ajoute les informations ARP privilégiées aux résultats Nmap utilisateur,
|
||||||
les privilèges ne peut pas réduire le nombre d'hôtes détectés.
|
sans relancer une seconde découverte complète.
|
||||||
"""
|
"""
|
||||||
span = max(20, end_percent - start_percent)
|
span = max(20, end_percent - start_percent)
|
||||||
arp_end = start_percent + round(span * 0.20)
|
arp_end = start_percent + round(span * 0.20)
|
||||||
normal_end = start_percent + round(span * 0.62)
|
normal_end = start_percent + round(span * 0.62)
|
||||||
admin_end = start_percent + round(span * 0.84)
|
|
||||||
neighbor_percent = start_percent + round(span * 0.94)
|
neighbor_percent = start_percent + round(span * 0.94)
|
||||||
|
|
||||||
local_hosts = self._emit_local_host()
|
local_hosts = self._emit_local_host()
|
||||||
@@ -777,34 +779,24 @@ class ScanWorker(QThread):
|
|||||||
"Découverte Nmap utilisateur échouée",
|
"Découverte Nmap utilisateur échouée",
|
||||||
start_percent=arp_end,
|
start_percent=arp_end,
|
||||||
end_percent=normal_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():
|
if self.isInterruptionRequested():
|
||||||
return union_host_ips(local_hosts, arp_hosts, normal_hosts)
|
return union_host_ips(local_hosts, arp_hosts, normal_hosts)
|
||||||
|
|
||||||
privileged_hosts: list[Host] = []
|
# Le mode Admin enrichit la découverte locale via ARP, mais ne relance pas
|
||||||
if self.request.privileged and not self.isInterruptionRequested():
|
# une seconde découverte Nmap identique. Les privilèges sont réservés aux
|
||||||
try:
|
# opérations qui en ont réellement besoin (SYN, OS et ARP).
|
||||||
cmd = privileged_command("nmap-discover", self.request.target)
|
known_ips = union_host_ips(arp_hosts, normal_hosts)
|
||||||
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)
|
|
||||||
known_ips.update(union_host_ips(local_hosts))
|
known_ips.update(union_host_ips(local_hosts))
|
||||||
if self.isInterruptionRequested():
|
if self.isInterruptionRequested():
|
||||||
return known_ips
|
return known_ips
|
||||||
self._neighbor_hosts(known_ips, percent=neighbor_percent)
|
self._neighbor_hosts(known_ips, percent=neighbor_percent)
|
||||||
details = f"local {len(local_hosts)} · ARP {len(arp_hosts)} · Nmap {len(normal_hosts)}"
|
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}")
|
self._set_progress(end_percent, f"Découverte : {len(known_ips)} hôte(s) unique(s) — {details}")
|
||||||
return known_ips
|
return known_ips
|
||||||
|
|
||||||
@@ -838,12 +830,18 @@ class ScanWorker(QThread):
|
|||||||
else:
|
else:
|
||||||
deep_args = [
|
deep_args = [
|
||||||
"nmap", "-Pn", "-n", "-sT", "-sV", "--version-light",
|
"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,
|
"-oX", "-", *ips,
|
||||||
]
|
]
|
||||||
deep_label = "Enrichissement approfondi — Nmap TCP et services…"
|
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_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:
|
if deep_hosts:
|
||||||
self.hosts_found.emit(deep_hosts)
|
self.hosts_found.emit(deep_hosts)
|
||||||
@@ -927,6 +925,7 @@ class HostScanWorker(QThread):
|
|||||||
else:
|
else:
|
||||||
args = [
|
args = [
|
||||||
"nmap", "-Pn", "-n", "-sT", "-sV", "--version-light", "--open", "-T4",
|
"nmap", "-Pn", "-n", "-sT", "-sV", "--version-light", "--open", "-T4",
|
||||||
|
"--max-retries", "1", "--host-timeout", "60s",
|
||||||
"--top-ports", "1000", "-oX", "-", self.ip,
|
"--top-ports", "1000", "-oX", "-", self.ip,
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
@@ -961,10 +960,16 @@ class HostScanWorker(QThread):
|
|||||||
for reader in readers:
|
for reader in readers:
|
||||||
reader.start()
|
reader.start()
|
||||||
stop_sent = False
|
stop_sent = False
|
||||||
|
started = time.monotonic()
|
||||||
|
timed_out = False
|
||||||
while self._proc.poll() is None:
|
while self._proc.poll() is None:
|
||||||
if self.isInterruptionRequested() and not stop_sent:
|
if self.isInterruptionRequested() and not stop_sent:
|
||||||
self.stop()
|
self.stop()
|
||||||
stop_sent = True
|
stop_sent = True
|
||||||
|
elif not stop_sent and time.monotonic() - started >= HOST_SCAN_MAX_SECONDS:
|
||||||
|
timed_out = True
|
||||||
|
self.stop()
|
||||||
|
stop_sent = True
|
||||||
try:
|
try:
|
||||||
self._proc.wait(timeout=0.10)
|
self._proc.wait(timeout=0.10)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
@@ -973,7 +978,26 @@ class HostScanWorker(QThread):
|
|||||||
reader.join(timeout=1.0)
|
reader.join(timeout=1.0)
|
||||||
stdout, stderr = "".join(stdout_parts), "".join(stderr_parts)
|
stdout, stderr = "".join(stdout_parts), "".join(stderr_parts)
|
||||||
else:
|
else:
|
||||||
|
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()
|
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():
|
if self.isInterruptionRequested():
|
||||||
return
|
return
|
||||||
if self._proc.returncode != 0:
|
if self._proc.returncode != 0:
|
||||||
|
|||||||
@@ -136,8 +136,9 @@ class PrivilegeAuthWorker(QThread):
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
errors="replace",
|
errors="replace",
|
||||||
check=False,
|
check=False,
|
||||||
|
timeout=30.0,
|
||||||
)
|
)
|
||||||
except (OSError, RuntimeError) as exc:
|
except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc:
|
||||||
self.result.emit(False, str(exc))
|
self.result.emit(False, str(exc))
|
||||||
return
|
return
|
||||||
if proc.returncode == 0:
|
if proc.returncode == 0:
|
||||||
@@ -1039,6 +1040,15 @@ class MainWindow(QMainWindow):
|
|||||||
self.statusBar().showMessage("Disposition réinitialisée", 3000)
|
self.statusBar().showMessage("Disposition réinitialisée", 3000)
|
||||||
|
|
||||||
def closeEvent(self, event) -> None:
|
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()
|
self._save_ui_layout()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ class EngineStatus0419Tests(unittest.TestCase):
|
|||||||
self.assertIn("découverte rapide Nmap/ARP", source)
|
self.assertIn("découverte rapide Nmap/ARP", source)
|
||||||
self.assertIn("ports Naabu SYN (Admin)", source)
|
self.assertIn("ports Naabu SYN (Admin)", source)
|
||||||
self.assertIn("ports Nmap SYN (Admin)", source)
|
self.assertIn("ports Nmap SYN (Admin)", source)
|
||||||
self.assertIn("REPLI Naabu", source)
|
self.assertIn("REPLI Nmap", source)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ class StandardBehavior0421Tests(unittest.TestCase):
|
|||||||
args = worker._nmap.call_args.args[0]
|
args = worker._nmap.call_args.args[0]
|
||||||
self.assertIn("-sV", args)
|
self.assertIn("-sV", args)
|
||||||
self.assertIn("1000", args)
|
self.assertIn("1000", args)
|
||||||
|
self.assertGreaterEqual(worker._nmap.call_args.kwargs["timeout_seconds"], 90.0)
|
||||||
|
|
||||||
|
|
||||||
class Diagnostics0421Tests(unittest.TestCase):
|
class Diagnostics0421Tests(unittest.TestCase):
|
||||||
|
|||||||
@@ -138,6 +138,30 @@ class PerformancePipeline0423Tests(unittest.TestCase):
|
|||||||
self.assertEqual(cmd[:6], ["nmap", "-sn", "-n", "-T4", "--max-retries", "1"])
|
self.assertEqual(cmd[:6], ["nmap", "-sn", "-n", "-T4", "--max-retries", "1"])
|
||||||
self.assertIsNotNone(worker._nmap.call_args.kwargs["timeout_seconds"])
|
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):
|
def test_naabu_failure_does_not_rescan_dead_addresses_with_nmap(self):
|
||||||
worker = self.worker()
|
worker = self.worker()
|
||||||
live = {f"192.168.10.{i}" for i in range(1, scanner.NAABU_ACTIVE_HOST_THRESHOLD + 1)}
|
live = {f"192.168.10.{i}" for i in range(1, scanner.NAABU_ACTIVE_HOST_THRESHOLD + 1)}
|
||||||
|
|||||||
Reference in New Issue
Block a user