Add RLUtils class for managing RL/AI dashboard endpoints

- Implemented methods for fetching AI stats, training history, and recent experiences.
- Added functionality to set operation mode (MANUAL, AUTO, AI) with appropriate handling.
- Included helper methods for querying the database and sending JSON responses.
- Integrated model metadata extraction for visualization purposes.
This commit is contained in:
Fabien POLLY
2026-02-18 22:36:10 +01:00
parent b8a13cc698
commit eb20b168a6
684 changed files with 53278 additions and 27977 deletions

View File

@@ -1,20 +1,24 @@
# scanning.py Network scanner (DB-first, no stubs)
# - Host discovery (nmap -sn -PR)
# - Resolve MAC/hostname (per-host threads) -> DB (hosts table)
# - Port scan (multi-threads) -> DB (merge ports by MAC)
# - Resolve MAC/hostname (ThreadPoolExecutor) -> DB (hosts table)
# - Port scan (ThreadPoolExecutor) -> DB (merge ports by MAC)
# - Mark alive=0 for hosts not seen this run
# - Update stats (stats table)
# - Light logging (milestones) without flooding
# - WAL checkpoint(TRUNCATE) + PRAGMA optimize at end of scan
# - NEW: No DB insert without a real MAC. Unresolved IPs are kept in-memory for this run.
# - No DB insert without a real MAC. Unresolved IPs are kept in-memory.
# - RPi Zero optimized: bounded thread pools, reduced retries, adaptive concurrency
import os
import re
import threading
import socket
import time
import logging
import subprocess
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
import datetime
import netifaces
from getmac import get_mac_address as gma
@@ -35,12 +39,48 @@ b_action = "global"
b_trigger = "on_interval:180"
b_requires = '{"max_concurrent": 1}'
# --- Module-level constants (avoid re-creating per call) ---
_MAC_RE = re.compile(r'([0-9A-Fa-f]{2})([-:])(?:[0-9A-Fa-f]{2}\2){4}[0-9A-Fa-f]{2}')
_BAD_MACS = frozenset({"00:00:00:00:00:00", "ff:ff:ff:ff:ff:ff"})
# RPi Zero safe defaults (overridable via shared config)
_MAX_HOST_THREADS = 2
_MAX_PORT_THREADS = 4
_PORT_TIMEOUT = 0.8
_MAC_RETRIES = 2
_MAC_RETRY_DELAY = 0.5
_ARPING_TIMEOUT = 1.0
_NMAP_DISCOVERY_TIMEOUT_S = 90
_NMAP_DISCOVERY_ARGS = "-sn -PR --max-retries 1 --host-timeout 8s"
_SCAN_MIN_INTERVAL_S = 600
def _normalize_mac(s):
if not s:
return None
m = _MAC_RE.search(str(s))
if not m:
return None
return m.group(0).replace('-', ':').lower()
def _is_bad_mac(mac):
if not mac:
return True
mac_l = mac.lower()
if mac_l in _BAD_MACS:
return True
parts = mac_l.split(':')
if len(parts) == 6 and len(set(parts)) == 1:
return True
return False
class NetworkScanner:
"""
Network scanner that populates SQLite (hosts + stats). No CSV/JSON.
Keeps the original fast logic: nmap discovery, per-host threads, per-port threads.
NEW: no 'IP:<ip>' stubs are ever written to the DB; unresolved IPs are tracked in-memory.
Uses ThreadPoolExecutor for bounded concurrency (RPi Zero safe).
No 'IP:<ip>' stubs are ever written to the DB; unresolved IPs are tracked in-memory.
"""
def __init__(self, shared_data):
self.shared_data = shared_data
@@ -52,8 +92,26 @@ class NetworkScanner:
self.lock = threading.Lock()
self.nm = nmap.PortScanner()
self.running = False
# Local stop flag for this action instance.
# IMPORTANT: actions must never mutate shared_data.orchestrator_should_exit (global stop signal).
self._stop_event = threading.Event()
self.thread = None
self.scan_interface = None
cfg = getattr(self.shared_data, "config", {}) or {}
self.max_host_threads = max(1, min(8, int(cfg.get("scan_max_host_threads", _MAX_HOST_THREADS))))
self.max_port_threads = max(1, min(16, int(cfg.get("scan_max_port_threads", _MAX_PORT_THREADS))))
self.port_timeout = max(0.3, min(3.0, float(cfg.get("scan_port_timeout_s", _PORT_TIMEOUT))))
self.mac_retries = max(1, min(5, int(cfg.get("scan_mac_retries", _MAC_RETRIES))))
self.mac_retry_delay = max(0.2, min(2.0, float(cfg.get("scan_mac_retry_delay_s", _MAC_RETRY_DELAY))))
self.arping_timeout = max(1.0, min(5.0, float(cfg.get("scan_arping_timeout_s", _ARPING_TIMEOUT))))
self.discovery_timeout_s = max(
20, min(300, int(cfg.get("scan_nmap_discovery_timeout_s", _NMAP_DISCOVERY_TIMEOUT_S)))
)
self.discovery_args = str(cfg.get("scan_nmap_discovery_args", _NMAP_DISCOVERY_ARGS)).strip() or _NMAP_DISCOVERY_ARGS
self.scan_min_interval_s = max(60, int(cfg.get("scan_min_interval_s", _SCAN_MIN_INTERVAL_S)))
self._last_scan_started = 0.0
# progress
self.total_hosts = 0
self.scanned_hosts = 0
@@ -76,9 +134,13 @@ class NetworkScanner:
total = min(max(total, 0), 100)
self.shared_data.bjorn_progress = f"{int(total)}%"
def _should_stop(self) -> bool:
# Treat orchestrator flag as read-only, and combine with local stop event.
return bool(getattr(self.shared_data, "orchestrator_should_exit", False)) or self._stop_event.is_set()
# ---------- network ----------
def get_network(self):
if self.shared_data.orchestrator_should_exit:
if self._should_stop():
return None
try:
if self.shared_data.use_custom_network:
@@ -118,7 +180,7 @@ class NetworkScanner:
self.logger.debug(f"nmap_prefixes not found at {path}")
return vendor_map
try:
with open(path, 'r') as f:
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
@@ -139,8 +201,11 @@ class NetworkScanner:
def get_current_essid(self):
try:
essid = subprocess.check_output(['iwgetid', '-r'], stderr=subprocess.STDOUT, universal_newlines=True).strip()
return essid or ""
result = subprocess.run(
['iwgetid', '-r'],
capture_output=True, text=True, timeout=5
)
return (result.stdout or "").strip()
except Exception:
return ""
@@ -160,57 +225,34 @@ class NetworkScanner:
Try multiple strategies to resolve a real MAC for the given IP.
RETURNS: normalized MAC like 'aa:bb:cc:dd:ee:ff' or None.
NEVER returns 'IP:<ip>'.
RPi Zero: reduced retries and timeouts.
"""
if self.shared_data.orchestrator_should_exit:
if self._should_stop():
return None
import re
MAC_RE = re.compile(r'([0-9A-Fa-f]{2})([-:])(?:[0-9A-Fa-f]{2}\2){4}[0-9A-Fa-f]{2}')
BAD_MACS = {"00:00:00:00:00:00", "ff:ff:ff:ff:ff:ff"}
def _normalize_mac(s: str | None) -> str | None:
if not s:
return None
m = MAC_RE.search(s)
if not m:
return None
return m.group(0).replace('-', ':').lower()
def _is_bad_mac(mac: str | None) -> bool:
if not mac:
return True
mac_l = mac.lower()
if mac_l in BAD_MACS:
return True
parts = mac_l.split(':')
if len(parts) == 6 and len(set(parts)) == 1:
return True
return False
try:
mac = None
# 1) getmac (retry a few times)
retries = 6
while not mac and retries > 0 and not self.shared_data.orchestrator_should_exit:
# 1) getmac (reduced retries for RPi Zero)
retries = self.mac_retries
while not mac and retries > 0 and not self._should_stop():
try:
from getmac import get_mac_address as gma
mac = _normalize_mac(gma(ip=ip))
except Exception:
mac = None
if not mac:
time.sleep(1.5)
time.sleep(self.mac_retry_delay)
retries -= 1
# 2) targeted arp-scan
if not mac:
if not mac and not self._should_stop():
try:
iface = self.scan_interface or self.shared_data.default_network_interface or "wlan0"
out = subprocess.check_output(
result = subprocess.run(
['sudo', 'arp-scan', '--interface', iface, '-q', ip],
universal_newlines=True, stderr=subprocess.STDOUT
capture_output=True, text=True, timeout=5
)
out = result.stdout or ""
for line in out.splitlines():
if line.strip().startswith(ip):
cand = _normalize_mac(line)
@@ -225,11 +267,13 @@ class NetworkScanner:
self.logger.debug(f"arp-scan fallback failed for {ip}: {e}")
# 3) ip neigh
if not mac:
if not mac and not self._should_stop():
try:
neigh = subprocess.check_output(['ip', 'neigh', 'show', ip],
universal_newlines=True, stderr=subprocess.STDOUT)
cand = _normalize_mac(neigh)
result = subprocess.run(
['ip', 'neigh', 'show', ip],
capture_output=True, text=True, timeout=3
)
cand = _normalize_mac(result.stdout or "")
if cand:
mac = cand
except Exception:
@@ -247,6 +291,7 @@ class NetworkScanner:
# ---------- port scanning ----------
class PortScannerWorker:
"""Port scanner using ThreadPoolExecutor for RPi Zero safety."""
def __init__(self, outer, target, open_ports, portstart, portend, extra_ports):
self.outer = outer
self.target = target
@@ -256,10 +301,10 @@ class NetworkScanner:
self.extra_ports = [int(p) for p in (extra_ports or [])]
def scan_one(self, port):
if self.outer.shared_data.orchestrator_should_exit:
if self.outer._should_stop():
return
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
s.settimeout(self.outer.port_timeout)
try:
s.connect((self.target, port))
with self.outer.lock:
@@ -274,25 +319,25 @@ class NetworkScanner:
self.outer.update_progress('port', 1)
def run(self):
if self.outer.shared_data.orchestrator_should_exit:
if self.outer._should_stop():
return
threads = []
for port in range(self.portstart, self.portend):
if self.outer.shared_data.orchestrator_should_exit:
break
t = threading.Thread(target=self.scan_one, args=(port,))
t.start()
threads.append(t)
for port in self.extra_ports:
if self.outer.shared_data.orchestrator_should_exit:
break
t = threading.Thread(target=self.scan_one, args=(port,))
t.start()
threads.append(t)
for t in threads:
if self.outer.shared_data.orchestrator_should_exit:
break
t.join()
ports = list(range(self.portstart, self.portend)) + self.extra_ports
if not ports:
return
with ThreadPoolExecutor(max_workers=self.outer.max_port_threads) as pool:
futures = []
for port in ports:
if self.outer._should_stop():
break
futures.append(pool.submit(self.scan_one, port))
for f in as_completed(futures):
if self.outer._should_stop():
break
try:
f.result(timeout=self.outer.port_timeout + 1)
except Exception:
pass
# ---------- main scan block ----------
class ScanPorts:
@@ -310,20 +355,28 @@ class NetworkScanner:
self.extra_ports = [int(p) for p in (extra_ports or [])]
self.ip_data = self.IpData()
self.ip_hostname_list = [] # tuples (ip, hostname, mac)
self.host_threads = []
self.open_ports = {}
self.all_ports = []
# NEW: per-run pending cache for unresolved IPs (no DB writes)
# ip -> {'hostnames': set(), 'ports': set(), 'first_seen': ts, 'essid': str}
# per-run pending cache for unresolved IPs (no DB writes)
self.pending = {}
def scan_network_and_collect(self):
if self.outer.shared_data.orchestrator_should_exit:
if self.outer._should_stop():
return
with self.outer.lock:
self.outer.shared_data.bjorn_progress = "1%"
t0 = time.time()
try:
self.outer.nm.scan(
hosts=str(self.network),
arguments=self.outer.discovery_args,
timeout=self.outer.discovery_timeout_s,
)
except Exception as e:
self.outer.logger.error(f"Nmap host discovery failed: {e}")
return
t0 = time.time()
self.outer.nm.scan(hosts=str(self.network), arguments='-sn -PR')
hosts = list(self.outer.nm.all_hosts())
if self.outer.blacklistcheck:
hosts = [ip for ip in hosts if ip not in self.outer.ip_scan_blacklist]
@@ -331,10 +384,23 @@ class NetworkScanner:
self.outer.total_hosts = len(hosts)
self.outer.scanned_hosts = 0
self.outer.update_progress('host', 0)
self.outer.logger.info(f"Host discovery: {len(hosts)} candidate(s) (took {time.time()-t0:.1f}s)")
elapsed = time.time() - t0
self.outer.logger.info(f"Host discovery: {len(hosts)} candidate(s) (took {elapsed:.1f}s)")
# Update comment for display
self.outer.shared_data.comment_params = {
"hosts_found": str(len(hosts)),
"network": str(self.network),
"elapsed": f"{elapsed:.1f}"
}
# existing hosts (for quick merge)
existing_rows = self.outer.shared_data.db.get_all_hosts()
try:
existing_rows = self.outer.shared_data.db.get_all_hosts()
except Exception as e:
self.outer.logger.error(f"DB get_all_hosts failed: {e}")
existing_rows = []
self.existing_map = {h['mac_address']: h for h in existing_rows}
self.seen_now = set()
@@ -342,19 +408,24 @@ class NetworkScanner:
self.vendor_map = self.outer.load_mac_vendor_map()
self.essid = self.outer.get_current_essid()
# per-host threads
for host in hosts:
if self.outer.shared_data.orchestrator_should_exit:
return
t = threading.Thread(target=self.scan_host, args=(host,))
t.start()
self.host_threads.append(t)
# per-host threads with bounded pool
max_threads = min(self.outer.max_host_threads, len(hosts)) if hosts else 1
with ThreadPoolExecutor(max_workers=max_threads) as pool:
futures = {}
for host in hosts:
if self.outer._should_stop():
break
f = pool.submit(self.scan_host, host)
futures[f] = host
# wait
for t in self.host_threads:
if self.outer.shared_data.orchestrator_should_exit:
return
t.join()
for f in as_completed(futures):
if self.outer._should_stop():
break
try:
f.result(timeout=30)
except Exception as e:
ip = futures.get(f, "?")
self.outer.logger.error(f"Host scan thread failed for {ip}: {e}")
self.outer.logger.info(
f"Host mapping completed: {self.outer.scanned_hosts}/{self.outer.total_hosts} processed, "
@@ -364,7 +435,10 @@ class NetworkScanner:
# mark unseen as alive=0
existing_macs = set(self.existing_map.keys())
for mac in existing_macs - self.seen_now:
self.outer.shared_data.db.update_host(mac_address=mac, alive=0)
try:
self.outer.shared_data.db.update_host(mac_address=mac, alive=0)
except Exception as e:
self.outer.logger.error(f"Failed to mark {mac} as dead: {e}")
# feed ip_data
for ip, hostname, mac in self.ip_hostname_list:
@@ -373,13 +447,19 @@ class NetworkScanner:
self.ip_data.mac_list.append(mac)
def scan_host(self, ip):
if self.outer.shared_data.orchestrator_should_exit:
if self.outer._should_stop():
return
if self.outer.blacklistcheck and ip in self.outer.ip_scan_blacklist:
return
try:
# ARP ping to help populate neighbor cache
os.system(f"arping -c 2 -w 2 {ip} > /dev/null 2>&1")
# ARP ping to help populate neighbor cache (subprocess with timeout)
try:
subprocess.run(
['arping', '-c', '2', '-w', str(self.outer.arping_timeout), ip],
capture_output=True, timeout=self.outer.arping_timeout + 2
)
except Exception:
pass
# Hostname (validated)
hostname = ""
@@ -393,7 +473,7 @@ class NetworkScanner:
self.outer.update_progress('host', 1)
return
time.sleep(1.0) # let ARP breathe
time.sleep(0.5) # let ARP breathe (reduced from 1.0 for RPi Zero speed)
mac = self.outer.get_mac_address(ip, hostname)
if mac:
@@ -431,10 +511,12 @@ class NetworkScanner:
if ip:
ips_set.add(ip)
# Update current hostname + track history
current_hn = ""
if hostname:
self.outer.shared_data.db.update_hostname(mac, hostname)
try:
self.outer.shared_data.db.update_hostname(mac, hostname)
except Exception as e:
self.outer.logger.error(f"Failed to update hostname for {mac}: {e}")
current_hn = hostname
else:
current_hn = (prev.get('hostnames') or "").split(';', 1)[0] if prev else ""
@@ -444,15 +526,18 @@ class NetworkScanner:
key=lambda x: tuple(map(int, x.split('.'))) if x.count('.') == 3 else (0, 0, 0, 0)
)) if ips_set else None
self.outer.shared_data.db.update_host(
mac_address=mac,
ips=ips_sorted,
hostnames=None,
alive=1,
ports=None,
vendor=vendor or (prev.get('vendor') if prev else ""),
essid=self.essid or (prev.get('essid') if prev else None)
)
try:
self.outer.shared_data.db.update_host(
mac_address=mac,
ips=ips_sorted,
hostnames=None,
alive=1,
ports=None,
vendor=vendor or (prev.get('vendor') if prev else ""),
essid=self.essid or (prev.get('essid') if prev else None)
)
except Exception as e:
self.outer.logger.error(f"Failed to update host {mac}: {e}")
# refresh local cache
self.existing_map[mac] = dict(
@@ -467,19 +552,26 @@ class NetworkScanner:
with self.outer.lock:
self.ip_hostname_list.append((ip, hostname or "", mac))
# Update comment params for live display
self.outer.shared_data.comment_params = {
"ip": ip, "mac": mac,
"hostname": hostname or "unknown",
"vendor": vendor or "unknown"
}
self.outer.logger.debug(f"MAC for {ip}: {mac} (hostname: {hostname or '-'})")
except Exception as e:
self.outer.logger.error(f"Error scanning host {ip}: {e}")
finally:
self.outer.update_progress('host', 1)
time.sleep(0.05)
time.sleep(0.02) # reduced from 0.05
def start(self):
if self.outer.shared_data.orchestrator_should_exit:
if self.outer._should_stop():
return
self.scan_network_and_collect()
if self.outer.shared_data.orchestrator_should_exit:
if self.outer._should_stop():
return
# init structures for ports
@@ -496,12 +588,22 @@ class NetworkScanner:
f"(+{len(self.extra_ports)} extra)"
)
# per-IP port scan (threads per port, original logic)
for idx, ip in enumerate(self.ip_data.ip_list, 1):
if self.outer.shared_data.orchestrator_should_exit:
if self.outer._should_stop():
return
worker = self.outer.PortScannerWorker(self.outer, ip, self.open_ports, self.portstart, self.portend, self.extra_ports)
# Update comment params for live display
self.outer.shared_data.comment_params = {
"ip": ip, "progress": f"{idx}/{total_targets}",
"ports_found": str(sum(len(v) for v in self.open_ports.values()))
}
worker = self.outer.PortScannerWorker(
self.outer, ip, self.open_ports,
self.portstart, self.portend, self.extra_ports
)
worker.run()
if idx % 10 == 0 or idx == total_targets:
found = sum(len(v) for v in self.open_ports.values())
self.outer.logger.info(
@@ -517,13 +619,27 @@ class NetworkScanner:
# ---------- orchestration ----------
def scan(self):
self.shared_data.orchestrator_should_exit = False
# Reset only local stop flag for this action. Never touch orchestrator_should_exit here.
self._stop_event.clear()
try:
if self.shared_data.orchestrator_should_exit:
if self._should_stop():
self.logger.info("Orchestrator switched to manual mode. Stopping scanner.")
return
now = time.time()
elapsed = now - self._last_scan_started if self._last_scan_started else 1e9
if elapsed < self.scan_min_interval_s:
remaining = int(self.scan_min_interval_s - elapsed)
self.logger.info_throttled(
f"Network scan skipped (min interval active, remaining={remaining}s)",
key="scanner_min_interval_skip",
interval_s=15.0,
)
return
self._last_scan_started = now
self.shared_data.bjorn_orch_status = "NetworkScanner"
self.shared_data.comment_params = {}
self.logger.info("Starting Network Scanner")
# network
@@ -535,6 +651,7 @@ class NetworkScanner:
return
self.shared_data.bjorn_status_text2 = str(network)
self.shared_data.comment_params = {"network": str(network)}
portstart = int(self.shared_data.portstart)
portend = int(self.shared_data.portend)
extra_ports = self.shared_data.portlist
@@ -547,21 +664,22 @@ class NetworkScanner:
ip_data, open_ports_by_ip, all_ports, alive_macs = result
if self.shared_data.orchestrator_should_exit:
if self._should_stop():
self.logger.info("Scan canceled before DB finalization.")
return
# push ports -> DB (merge by MAC). Only for IPs with known MAC.
# map ip->mac
# push ports -> DB (merge by MAC)
ip_to_mac = {ip: mac for ip, _, mac in zip(ip_data.ip_list, ip_data.hostname_list, ip_data.mac_list)}
# existing cache
existing_map = {h['mac_address']: h for h in self.shared_data.db.get_all_hosts()}
try:
existing_map = {h['mac_address']: h for h in self.shared_data.db.get_all_hosts()}
except Exception as e:
self.logger.error(f"DB get_all_hosts for port merge failed: {e}")
existing_map = {}
for ip, ports in open_ports_by_ip.items():
mac = ip_to_mac.get(ip)
if not mac:
# store to pending (no DB write)
slot = scanner.pending.setdefault(
ip,
{'hostnames': set(), 'ports': set(), 'first_seen': int(time.time()), 'essid': scanner.essid}
@@ -578,16 +696,19 @@ class NetworkScanner:
pass
ports_set.update(str(p) for p in (ports or []))
self.shared_data.db.update_host(
mac_address=mac,
ports=';'.join(sorted(ports_set, key=lambda x: int(x))),
alive=1
)
try:
self.shared_data.db.update_host(
mac_address=mac,
ports=';'.join(sorted(ports_set, key=lambda x: int(x))),
alive=1
)
except Exception as e:
self.logger.error(f"Failed to update ports for {mac}: {e}")
# Late resolution pass: try to resolve pending IPs before stats
# Late resolution pass
unresolved_before = len(scanner.pending)
for ip, data in list(scanner.pending.items()):
if self.shared_data.orchestrator_should_exit:
if self._should_stop():
break
try:
guess_hostname = next(iter(data['hostnames']), "")
@@ -595,25 +716,28 @@ class NetworkScanner:
guess_hostname = ""
mac = self.get_mac_address(ip, guess_hostname)
if not mac:
continue # still unresolved for this run
continue
mac = mac.lower()
vendor = self.mac_to_vendor(mac, scanner.vendor_map)
# create/update host now
self.shared_data.db.update_host(
mac_address=mac,
ips=ip,
hostnames=';'.join(data['hostnames']) or None,
vendor=vendor,
essid=data.get('essid'),
alive=1
)
if data['ports']:
try:
self.shared_data.db.update_host(
mac_address=mac,
ports=';'.join(str(p) for p in sorted(data['ports'], key=int)),
ips=ip,
hostnames=';'.join(data['hostnames']) or None,
vendor=vendor,
essid=data.get('essid'),
alive=1
)
if data['ports']:
self.shared_data.db.update_host(
mac_address=mac,
ports=';'.join(str(p) for p in sorted(data['ports'], key=int)),
alive=1
)
except Exception as e:
self.logger.error(f"Failed to resolve pending IP {ip}: {e}")
continue
del scanner.pending[ip]
if scanner.pending:
@@ -622,8 +746,13 @@ class NetworkScanner:
f"(resolved during late pass: {unresolved_before - len(scanner.pending)})"
)
# stats (alive, total ports, distinct vulnerabilities on alive)
rows = self.shared_data.db.get_all_hosts()
# stats
try:
rows = self.shared_data.db.get_all_hosts()
except Exception as e:
self.logger.error(f"DB get_all_hosts for stats failed: {e}")
rows = []
alive_hosts = [r for r in rows if int(r.get('alive') or 0) == 1]
all_known = len(rows)
@@ -641,12 +770,23 @@ class NetworkScanner:
except Exception:
vulnerabilities_count = 0
self.shared_data.db.set_stats(
total_open_ports=total_open_ports,
alive_hosts_count=len(alive_hosts),
all_known_hosts_count=all_known,
vulnerabilities_count=int(vulnerabilities_count)
)
try:
self.shared_data.db.set_stats(
total_open_ports=total_open_ports,
alive_hosts_count=len(alive_hosts),
all_known_hosts_count=all_known,
vulnerabilities_count=int(vulnerabilities_count)
)
except Exception as e:
self.logger.error(f"Failed to set stats: {e}")
# Update comment params with final stats
self.shared_data.comment_params = {
"alive_hosts": str(len(alive_hosts)),
"total_ports": str(total_open_ports),
"vulns": str(int(vulnerabilities_count)),
"network": str(network)
}
# WAL checkpoint + optimize
try:
@@ -661,7 +801,7 @@ class NetworkScanner:
self.logger.info("Network scan complete (DB updated).")
except Exception as e:
if self.shared_data.orchestrator_should_exit:
if self._should_stop():
self.logger.info("Orchestrator switched to manual mode. Gracefully stopping the network scanner.")
else:
self.logger.error(f"Error in scan: {e}")
@@ -673,7 +813,9 @@ class NetworkScanner:
def start(self):
if not self.running:
self.running = True
self.thread = threading.Thread(target=self.scan_wrapper, daemon=True)
self._stop_event.clear()
# Non-daemon so orchestrator can join it reliably (no orphan thread).
self.thread = threading.Thread(target=self.scan_wrapper, daemon=False)
self.thread.start()
logger.info("NetworkScanner started.")
@@ -683,25 +825,22 @@ class NetworkScanner:
finally:
with self.lock:
self.shared_data.bjorn_progress = ""
self.running = False
logger.debug("bjorn_progress reset to empty string")
def stop(self):
if self.running:
self.running = False
self.shared_data.orchestrator_should_exit = True
self._stop_event.set()
try:
if hasattr(self, "thread") and self.thread.is_alive():
self.thread.join()
self.thread.join(timeout=15)
except Exception:
pass
logger.info("NetworkScanner stopped.")
if __name__ == "__main__":
# SharedData must provide .db (BjornDatabase) and fields:
# default_network_interface, use_custom_network, custom_network,
# portstart, portend, portlist, blacklistcheck, mac/ip/hostname blacklists,
# bjorn_progress, bjorn_orch_status, bjorn_status_text2, orchestrator_should_exit.
from shared import SharedData
sd = SharedData()
scanner = NetworkScanner(sd)