Files
librenet-scanner/src/librenet_scanner/fastscan.py
T
2026-08-23 09:52:32 +02:00

143 lines
4.8 KiB
Python

from __future__ import annotations
import ipaddress
import json
import os
from dataclasses import dataclass
from .intelligence import canonical_service_name, enrich_host
from .models import Host, PortInfo
from .naabu_runtime import BUNDLED_NAABU_PATH, NAABU_VERSION, naabu_version, trusted_root_binary
SYSTEM_NAABU_CANDIDATES = (BUNDLED_NAABU_PATH,)
@dataclass(slots=True, frozen=True)
class NaabuDiagnostic:
user_path: str | None
admin_path: str | None
user_detail: str
admin_detail: str
def find_naabu() -> str | None:
"""Retourne exclusivement le moteur Naabu provisionné par LibreNet.
Le paquet peut provisionner un Naabu 2.6.1 dans l'espace privé de
LibreNet comme accélérateur optionnel pour les grands ensembles d'hôtes. Ne jamais choisir silencieusement un ``naabu`` du PATH évite qu'une
version différente modifie les options ou le comportement du profil Standard.
Si le moteur intégré est absent ou corrompu, le scanner reste pleinement
utilisable avec Nmap au lieu d'utiliser un binaire inconnu.
"""
candidate = BUNDLED_NAABU_PATH
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
if naabu_version(candidate) == NAABU_VERSION:
return candidate
return None
def _trusted_system_naabu(path: str) -> bool:
"""Même règle de confiance que le helper root, sans exécuter de privilèges."""
return trusted_root_binary(path)
def find_admin_naabu() -> str | None:
"""Retourne le moteur LibreNet exact et sûr acceptable par le helper Polkit."""
candidate = BUNDLED_NAABU_PATH
if _trusted_system_naabu(candidate) and naabu_version(candidate) == NAABU_VERSION:
return candidate
return None
def naabu_diagnostic() -> NaabuDiagnostic:
user_path = find_naabu()
admin_path = find_admin_naabu()
if user_path:
version = naabu_version(user_path)
suffix = f" — v{version}" if version else " — version non confirmée"
source = "moteur LibreNet intégré" if user_path == BUNDLED_NAABU_PATH else "moteur externe"
user_detail = f"OK — {source}{suffix}"
else:
user_detail = "INDISPONIBLE — Nmap sera utilisé (fonctionnement normal)"
if admin_path:
version = naabu_version(admin_path)
suffix = f", v{version}" if version else ", version non confirmée"
source = "moteur LibreNet intégré" if admin_path == BUNDLED_NAABU_PATH else "moteur externe"
admin_detail = f"OK — {source}, root, non modifiable par groupe/autres{suffix}"
elif user_path:
admin_detail = (
"INDISPONIBLE — un Naabu utilisateur existe, mais le mode Admin exige "
"le moteur LibreNet ou un Naabu système appartenant à root"
)
else:
admin_detail = "INDISPONIBLE — aucun Naabu système de confiance"
return NaabuDiagnostic(user_path, admin_path, user_detail, admin_detail)
def _naabu_payload(line: str) -> dict | None:
try:
payload = json.loads(line)
except (TypeError, json.JSONDecodeError):
return None
return payload if isinstance(payload, dict) else None
def _payload_ipv4(payload: dict) -> str | None:
value = str(payload.get("ip") or payload.get("host") or "").strip()
try:
address = ipaddress.ip_address(value)
except ValueError:
return None
if address.version != 4:
return None
return str(address)
def parse_naabu_host_json_line(line: str) -> Host | None:
"""Parse une ligne JSONL issue de ``naabu -sn -json``.
Depuis Naabu 2.4, un scan de découverte JSON émet aussi les résultats sans port.
LibreNet n'a besoin ici que de l'IPv4 : l'équipement est marqué actif et sera
enrichi par ARP/neighbor/ports lors des phases suivantes.
"""
payload = _naabu_payload(line)
if payload is None:
return None
ip = _payload_ipv4(payload)
if not ip:
return None
return Host(ip=ip, status="up")
def parse_naabu_json_line(line: str) -> Host | None:
"""Transforme une ligne JSONL de scan de ports Naabu en observation LibreNet."""
payload = _naabu_payload(line)
if payload is None:
return None
ip = _payload_ipv4(payload)
if not ip:
return None
try:
port = int(payload.get("port"))
except (ValueError, TypeError):
return None
if not 1 <= port <= 65535:
return None
protocol = str(payload.get("protocol") or "tcp").strip().lower()
if protocol not in {"tcp", "udp"}:
protocol = "tcp"
service = str(payload.get("service") or "").strip()
host = Host(
ip=ip,
status="up",
ports=[
PortInfo(
port=port,
protocol=protocol,
state="open",
service=canonical_service_name(port, protocol, service),
)
],
)
return enrich_host(host)