diff --git a/network_scanner/gui.py b/network_scanner/gui.py index 21d828b..7acbaf2 100644 --- a/network_scanner/gui.py +++ b/network_scanner/gui.py @@ -3,8 +3,7 @@ import threading import os import asyncio -from concurrent.futures import ThreadPoolExecutor, as_completed -from .scanner import check_npcap, load_mac_prefixes, get_ip_range, scan_network, get_mac_vendor, logger +from .scanner import check_npcap, load_mac_prefixes, get_ip_range, scan_network, logger from .block import (block_via_arp_poison, block_via_arp_flood, block_via_arp_tornado, block_via_mac_flood, block_via_icmp_unreachable, block_via_tcp_syn_flood, block_via_dns_amplification) @@ -246,7 +245,8 @@ def update_treeview_columns(self): def refresh_treeview_data(self): for device in self.current_devices: row_data = self.build_row_data(device) - self.tree.insert("", tk.END, values=row_data) + item_id = self.tree.insert("", tk.END, values=row_data) + device["tree_id"] = item_id def build_row_data(self, device): row = [device.get("ip", "Unknown"), device.get("mac", "Unknown")] @@ -260,6 +260,39 @@ def build_row_data(self, device): row.append(",".join(map(str, ports)) if ports else "None") return tuple(row) + def handle_device_found(self, device): + """Handle a newly discovered device during scanning. + + Insert the device into the Treeview immediately so the user sees + progress in real time. Additional details such as the OS guess and + open ports are gathered in a background thread and the row is updated + once the information is ready. + """ + + def process_device(): + try: + device["os_guess"] = fingerprint_os(device["ip"]) + except Exception: + device["os_guess"] = "Unknown" + try: + device["open_ports"] = probe_open_ports(device["ip"]) + except Exception: + device["open_ports"] = [] + + def update_row(): + self.tree.item(device["tree_id"], values=self.build_row_data(device)) + + self.master.after(0, update_row) + + def add_row(): + row_data = self.build_row_data(device) + item_id = self.tree.insert("", tk.END, values=row_data) + device["tree_id"] = item_id + self.current_devices.append(device) + threading.Thread(target=process_device, daemon=True).start() + + self.master.after(0, add_row) + # ----------------------- Scanning ----------------------- def run_scan(self): self.update_status("Starting network scan...") @@ -283,45 +316,15 @@ def thread_scan(self): ip_range = get_ip_range() self.network = ip_range - # Step 4: Scan the network asynchronously. - devices = asyncio.run(scan_network(ip_range, self.mac_prefixes)) - - # Initialize vendor and port info. - for device in devices: - device["vendor"] = "Not Fetched" - device["open_ports"] = [] - device["os_guess"] = "Unknown" + # Step 4: Scan the network asynchronously and report devices as found. + asyncio.run( + scan_network( + ip_range, + self.mac_prefixes, + progress_callback=self.handle_device_found, + ) + ) - # Step 5: Get vendor info if enabled. - if self.show_vendor_var.get(): - for device in devices: - device["vendor"] = get_mac_vendor(device.get("mac", ""), self.mac_prefixes) - - # Step 6: Get OS guess if enabled using advanced fingerprinting. - if self.show_os_var.get(): - with ThreadPoolExecutor(max_workers=10) as executor: - future_to_device = { - executor.submit(fingerprint_os, device["ip"]): device for device in devices - } - for future in as_completed(future_to_device): - dev = future_to_device[future] - try: - dev["os_guess"] = future.result() - except Exception: - dev["os_guess"] = "Unknown" - # Step 7: Scan open ports if enabled. - if self.show_port_var.get(): - with ThreadPoolExecutor(max_workers=10) as executor: - future_to_device = { - executor.submit(probe_open_ports, device["ip"]): device for device in devices - } - for future in as_completed(future_to_device): - dev = future_to_device[future] - try: - dev["open_ports"] = future.result() - except Exception: - dev["open_ports"] = [] - self.current_devices = devices self.master.after(0, self.post_scan_update) except Exception as e: logger.error(f"An error occurred during scanning: {e}") @@ -329,12 +332,12 @@ def thread_scan(self): self.master.after(0, lambda: self.update_status("Ready")) def post_scan_update(self): - self.refresh_treeview_data() self.scan_button.config(state="normal") if not self.current_devices: messagebox.showinfo("Scan Complete", "No devices found on the network.") else: messagebox.showinfo("Scan Complete", f"Found {len(self.current_devices)} device(s).") + self.update_status("Ready") # ----------------------- Blocking ----------------------- def update_block_parameters(self, *args): diff --git a/network_scanner/scanner.py b/network_scanner/scanner.py index 621cd1a..a8afaf9 100644 --- a/network_scanner/scanner.py +++ b/network_scanner/scanner.py @@ -163,8 +163,12 @@ def get_ip_range(): logger.error(f"Failed to determine local IP range: {e}") raise -async def scan_subnet(subnet, mac_prefixes, timeout=3, retry=2): - """Asynchronously scan a subnet for active devices.""" +async def scan_subnet(subnet, mac_prefixes, timeout=3, retry=2, progress_callback=None): + """Asynchronously scan a subnet for active devices. + + If ``progress_callback`` is provided it will be called for each device + as soon as it responds, allowing callers to react in real time. + """ results = [] if not check_npcap(): @@ -174,20 +178,7 @@ async def scan_subnet(subnet, mac_prefixes, timeout=3, retry=2): broadcast = Ether(dst="ff:ff:ff:ff:ff:ff") packet = broadcast / arp_req - try: - # Use AsyncSniffer without a timeout so we control when to stop it. - sniffer = AsyncSniffer(filter="arp and arp[6:2] == 2") - sniffer.start() - await asyncio.to_thread(sendp, packet, verbose=False) - await asyncio.sleep(timeout) - # sniffer.stop() returns the captured packets. - answered = sniffer.stop() - logger.info(f"Found {len(answered)} devices in subnet {subnet}") - except Exception as e: - logger.error(f"Error scanning subnet {subnet}: {e}") - return results - - for pkt in answered: + def handle_packet(pkt): if ARP in pkt and pkt[ARP].op == 2: device = { 'ip': pkt[ARP].psrc, @@ -196,10 +187,31 @@ async def scan_subnet(subnet, mac_prefixes, timeout=3, retry=2): 'device_name': get_device_name(pkt[ARP].psrc) } results.append(device) + if progress_callback: + progress_callback(device) + + try: + # Use AsyncSniffer with a packet handler to stream results immediately. + sniffer = AsyncSniffer( + filter="arp and arp[6:2] == 2", prn=handle_packet, store=False + ) + sniffer.start() + await asyncio.to_thread(sendp, packet, verbose=False) + await asyncio.sleep(timeout) + sniffer.stop() + logger.info(f"Found {len(results)} devices in subnet {subnet}") + except Exception as e: + logger.error(f"Error scanning subnet {subnet}: {e}") + return results -async def scan_network(ip_range, mac_prefixes, max_workers=10, subnet_prefix=24): - """Asynchronously scan a network by scanning subnets concurrently.""" +async def scan_network(ip_range, mac_prefixes, max_workers=10, subnet_prefix=24, + progress_callback=None): + """Asynchronously scan a network by scanning subnets concurrently. + + ``progress_callback`` is passed to each subnet scan and invoked for every + device as soon as it is discovered. + """ devices = [] try: subnets = list(ip_range.subnets(new_prefix=subnet_prefix)) @@ -211,16 +223,18 @@ async def scan_network(ip_range, mac_prefixes, max_workers=10, subnet_prefix=24) async def worker(subnet): async with semaphore: - return await scan_subnet(subnet, mac_prefixes) + return await scan_subnet( + subnet, mac_prefixes, progress_callback=progress_callback + ) tasks = [asyncio.create_task(worker(subnet)) for subnet in subnets] - results = await asyncio.gather(*tasks, return_exceptions=True) - for result in results: - if isinstance(result, Exception): - logger.error(f"Error processing subnet: {result}") - else: + for coro in asyncio.as_completed(tasks): + try: + result = await coro devices.extend(result) + except Exception as e: + logger.error(f"Error processing subnet: {e}") logger.info(f"Scan completed. Found {len(devices)} devices") except Exception as e: