From c60de37addfc1d05fab07aebc4a0f25bd1999952 Mon Sep 17 00:00:00 2001 From: Alex Amygdalios Date: Sat, 14 Jun 2025 17:00:44 +0300 Subject: [PATCH 1/6] Grouped CLI and GUI cuz why seperate into two branches? --- .DS_Store | Bin 0 -> 6148 bytes {Code => CLI}/netscan.py | 0 {Code => CLI}/port_scan.py | 0 {Code => CLI}/quick_scanner.py | 0 {Code => CLI}/scanner.py | 0 {Code => CLI}/sniffer.py | 0 GUI/.DS_Store | Bin 0 -> 6148 bytes GUI/app.py | 289 +++++++++++++++++++++++++++++++++ GUI/port_scan.py | 92 +++++++++++ GUI/scanner.py | 146 +++++++++++++++++ GUI/sniffer.py | 44 +++++ 11 files changed, 571 insertions(+) create mode 100644 .DS_Store rename {Code => CLI}/netscan.py (100%) rename {Code => CLI}/port_scan.py (100%) rename {Code => CLI}/quick_scanner.py (100%) rename {Code => CLI}/scanner.py (100%) rename {Code => CLI}/sniffer.py (100%) create mode 100644 GUI/.DS_Store create mode 100644 GUI/app.py create mode 100644 GUI/port_scan.py create mode 100644 GUI/scanner.py create mode 100644 GUI/sniffer.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c6ff94ac62a1f4f162b66202cb98db33d93bc20c GIT binary patch literal 6148 zcmeHK!Ab)`41H;PXz{Xm^0;46@DHYSdno8d4_>9#L!q^(Tktr);n(U*GD;WsD1wr} z*9k*4+#_W!&3QMulfQ;0Xt8%-V7Gh!!u*Zt;q?n?IN^ z|Mn=~jtvzI1Ovf9Fc1v%fwZ||wKw&u9wP|>)o28FhjVxpB| iPV^o*GpGA&wdqdy}Q89|0pIR50)h4153?eKa=! literal 0 HcmV?d00001 diff --git a/Code/netscan.py b/CLI/netscan.py similarity index 100% rename from Code/netscan.py rename to CLI/netscan.py diff --git a/Code/port_scan.py b/CLI/port_scan.py similarity index 100% rename from Code/port_scan.py rename to CLI/port_scan.py diff --git a/Code/quick_scanner.py b/CLI/quick_scanner.py similarity index 100% rename from Code/quick_scanner.py rename to CLI/quick_scanner.py diff --git a/Code/scanner.py b/CLI/scanner.py similarity index 100% rename from Code/scanner.py rename to CLI/scanner.py diff --git a/Code/sniffer.py b/CLI/sniffer.py similarity index 100% rename from Code/sniffer.py rename to CLI/sniffer.py diff --git a/GUI/.DS_Store b/GUI/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..592d85f607142088d463fd740cb3631ffcdbdc4b GIT binary patch literal 6148 zcmeH~F>b>!3`IXv4+gS$%&4UY=naH4JwY!JwC-lWLy=ua&nLwtZs)=%J^}KHlnL8^ zuuK4UzRfSd1Yo2)@$6w^#(cmlR?PUCzdx6cbzQtjJ9NNP`h?AXZVOUC3P=GdAO)nr zjugmad_3;xne-@9Knmh|QedONQ_m+q|KI4p&HtMgrBXl& z{4)h?xV&G^e5pKJf4rX8uUYkVqmywt!^2Mi6F-Vi^f2xhUywD~I$5FVM<8TSkOF_I Fz#UqC5`X{z literal 0 HcmV?d00001 diff --git a/GUI/app.py b/GUI/app.py new file mode 100644 index 0000000..6175cdf --- /dev/null +++ b/GUI/app.py @@ -0,0 +1,289 @@ +import tkinter as tk +from tkinter import ttk +from tkinter import messagebox +import threading +from datetime import datetime +import os +import sys +from scanner import scan +from port_scan import scan_ports +from sniffer import start_sniffing, stop_sniffing, process_packet, get_interface_list, packet_store # Import the updated functions +from scapy.utils import hexdump + +def check_admin(): + if os.name == 'nt': + # Windows + is_admin = os.system("net session >nul 2>&1") + if is_admin != 0: + show_warning() + sys.exit(0) + else: + # Unix/Linux/MacOS + is_admin = os.geteuid() == 0 + if not is_admin: + show_warning() + sys.exit(0) + +def show_warning(): + root = tk.Tk() + root.withdraw() # Hide the root window + messagebox.showwarning("Warning", "You must run this program as root/admin.") + root.destroy() + +def scan_network(): + try: + status_label.config(text="Scanning network...") + progress_bar.pack(pady=10, fill=tk.X) + progress_bar.start() + threading.Thread(target=perform_scan).start() + except Exception as e: + status_label.config(text=f"Error: {str(e)}") + progress_bar.stop() + progress_bar.pack_forget() + +def perform_scan(): + try: + save_file = save_file_var.get() + save_type = "" + if save_file: + if save_type_var.get() == 1: + save_type = "json" + elif save_type_var.get() == 2: + save_type = "xml" + elif save_type_var.get() == 3: + save_type = "txt" + + file_name = file_name_entry.get().strip() + if not file_name: + current_time = datetime.now() + file_name = f"scan-{current_time.strftime('%H-%M-%d-%m-%Y')}" + + sample_dict = scan(v_switch=True, vV_switch=True, save_file=save_file, save_type=save_type, file_name=file_name) + + for row in tree.get_children(): + tree.delete(row) + + for key, value in sample_dict.items(): + tree.insert("", "end", values=(key, value['ip'], value['mac_address'], value['device_name'], value['vendor'])) + + status_label.config(text="Scan complete.") + progress_bar.stop() + progress_bar.pack_forget() + except Exception as e: + status_label.config(text=f"Error: {str(e)}") + progress_bar.stop() + progress_bar.pack_forget() + +def show_hide_file_format(): + if save_file_var.get(): + json_radio.pack(anchor=tk.W) + xml_radio.pack(anchor=tk.W) + txt_radio.pack(anchor=tk.W) + file_name_label.pack(anchor=tk.W) + file_name_entry.pack(anchor=tk.W) + else: + json_radio.pack_forget() + xml_radio.pack_forget() + txt_radio.pack_forget() + file_name_label.pack_forget() + file_name_entry.pack_forget() + +def show_device_details(event): + selected_item = tree.selection()[0] + device_info = tree.item(selected_item, "values") + ip_address = device_info[1] + + detail_window = tk.Toplevel(root) + detail_window.title(f"Details for {device_info[3]} ({ip_address})") + detail_window.geometry("400x300") + + details = f"ID: {device_info[0]}\nIP Address: {ip_address}\nMAC Address: {device_info[2]}\nDevice Name: {device_info[3]}\nVendor: {device_info[4]}" + ttk.Label(detail_window, text=details).pack(pady=10) + + def run_port_scan(): + result = scan_ports(ip_address) + found_ports_str = "" + if result: + for found_port in result: + if found_ports_str: + found_ports_str += f"\nPort: {found_port[0]} {found_port[1]}" + else: + found_ports_str += f"\nPort: {found_port[0]} {found_port[1]}" + else: + found_ports_str = "No open ports found" + + ports_text.set(found_ports_str) + + ports_text = tk.StringVar() + ports_text.set("Ports will be displayed here.") + + ttk.Label(detail_window, text="Port Scan Results:").pack(pady=(10, 0)) + ttk.Label(detail_window, textvariable=ports_text).pack(pady=5) + ttk.Button(detail_window, text="Run Port Scan", command=run_port_scan).pack(pady=10) + +def start_packet_sniffing(): + try: + iface = interface_combobox.get().strip() + if not iface: + status_label_sniffer.config(text="Please select a network interface.") + return + + status_label_sniffer.config(text=f"Starting packet sniffing on interface {iface}...") + + # Clear existing entries in the Treeview + for item in tree_sniffer.get_children(): + tree_sniffer.delete(item) + + # Start packet sniffing and process each packet + threading.Thread(target=start_sniffing, args=(iface, lambda pkt: process_packet(pkt, tree_sniffer))).start() + + # Hide the start button once sniffing has started + start_button.pack_forget() + # Show the stop button + stop_button.pack(side=tk.RIGHT, padx=10) + except Exception as e: + status_label_sniffer.config(text=f"Error: {str(e)}") + +def stop_sniffing_action(): + stop_sniffing() + status_label_sniffer.config(text="Packet sniffer stopped.") + + # Hide the stop button after stopping sniffing + stop_button.pack_forget() + # Show the start button + start_button.pack(side=tk.RIGHT, padx=10) + +def show_packet_details(event): + selected_item = tree_sniffer.selection()[0] + pkt_id = int(selected_item) + packet = packet_store[pkt_id] + + detail_window = tk.Toplevel(root) + detail_window.title(f"Packet Details (ID: {pkt_id})") + detail_window.geometry("800x600") + + # Get packet summary + pkt_summary = packet.summary() + + # Parse packet layers and fields + pkt_details = [] + for layer in packet.layers(): + pkt_details.append(f"Layer: {layer}") + for field, value in packet[layer].fields.items(): + pkt_details.append(f"{field.capitalize()}: {value}") + pkt_details.append("") # Blank line for separation + + # Get hexadecimal dump of the packet + hex_dump = hexdump(packet, dump=True) + + summary_label = ttk.Label(detail_window, text=f"Packet Summary:\n{pkt_summary}") + summary_label.pack(pady=10) + + details_text = tk.Text(detail_window, wrap=tk.WORD) + details_text.insert(tk.END, "\n".join(pkt_details)) + details_text.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) + + hexdump_text = tk.Text(detail_window, wrap=tk.WORD) + hexdump_text.insert(tk.END, hex_dump) + hexdump_text.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) + +# Check if the user has admin rights before initializing the main application window +check_admin() + +# Set up the main application window +root = tk.Tk() +root.title("Netscan") +root.geometry("1000x700") + +# Create a notebook for tabs +notebook = ttk.Notebook(root) +notebook.pack(pady=10, expand=True, fill=tk.BOTH) + +# Create the network scan tab +network_scan_tab = ttk.Frame(notebook) +notebook.add(network_scan_tab, text="Network Scan") + +tree_frame = ttk.Frame(network_scan_tab) +tree_frame.pack(pady=10, fill=tk.BOTH, expand=True) + +columns = ("ID", "IP Address", "MAC Address", "Device Name", "Vendor") +tree = ttk.Treeview(tree_frame, columns=columns, show="headings") + +for col in columns: + tree.heading(col, text=col) + tree.column(col, anchor=tk.CENTER) + +tree.pack(fill=tk.BOTH, expand=True) + +tree.bind("", show_device_details) # Bind double-click event + +bottom_frame = ttk.Frame(network_scan_tab) +bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10) + +status_label = ttk.Label(bottom_frame, text="Click 'Scan network' to start.") +status_label.pack(side=tk.LEFT, padx=10) + +progress_bar = ttk.Progressbar(bottom_frame, mode='indeterminate') + +display_button = ttk.Button(bottom_frame, text="Scan network", command=scan_network) +display_button.pack(side=tk.RIGHT, padx=10) + +save_file_var = tk.BooleanVar() +save_file_checkbox = ttk.Checkbutton(bottom_frame, text="Save Scan Results", variable=save_file_var, command=show_hide_file_format) +save_file_checkbox.pack(side=tk.RIGHT, padx=10) + +save_type_var = tk.IntVar() +json_radio = ttk.Radiobutton(bottom_frame, text="JSON", variable=save_type_var, value=1) +xml_radio = ttk.Radiobutton(bottom_frame, text="XML", variable=save_type_var, value=2) +txt_radio = ttk.Radiobutton(bottom_frame, text="TXT", variable=save_type_var, value=3) + +file_name_label = ttk.Label(bottom_frame, text="File Name:") +file_name_entry = ttk.Entry(bottom_frame) + +# Hide these elements initially +json_radio.pack_forget() +xml_radio.pack_forget() +txt_radio.pack_forget() +file_name_label.pack_forget() +file_name_entry.pack_forget() + +# Create the packet sniffer tab +packet_sniffer_tab = ttk.Frame(notebook) +notebook.add(packet_sniffer_tab, text="Packet Sniffer") + +sniffer_frame = ttk.Frame(packet_sniffer_tab) +sniffer_frame.pack(pady=10, fill=tk.BOTH, expand=True) + +columns_sniffer = ("Time", "Source MAC", "Destination MAC", "Protocol") +tree_sniffer = ttk.Treeview(sniffer_frame, columns=columns_sniffer, show="headings") + +for col in columns_sniffer: + tree_sniffer.heading(col, text=col) + tree_sniffer.column(col, anchor=tk.CENTER) + +tree_sniffer.pack(fill=tk.BOTH, expand=True) + +tree_sniffer.bind("", show_packet_details) # Bind double-click event + +bottom_frame_sniffer = ttk.Frame(packet_sniffer_tab) +bottom_frame_sniffer.pack(side=tk.BOTTOM, fill=tk.X, pady=10) + +status_label_sniffer = ttk.Label(bottom_frame_sniffer, text="Select an interface and click 'Start Sniffing' to begin.") +status_label_sniffer.pack(side=tk.LEFT, padx=10) + +interface_label = ttk.Label(bottom_frame_sniffer, text="Interface:") +interface_label.pack(side=tk.LEFT, padx=10) + +interface_list = get_interface_list() +interface_combobox = ttk.Combobox(bottom_frame_sniffer, values=interface_list, state="readonly") +interface_combobox.pack(side=tk.LEFT, padx=10) +interface_combobox.set(interface_list[0] if interface_list else "No interfaces found") + +start_button = ttk.Button(bottom_frame_sniffer, text="Start Sniffing", command=start_packet_sniffing) +start_button.pack(side=tk.RIGHT, padx=10) + +stop_button = ttk.Button(bottom_frame_sniffer, text="Stop Sniffing", command=stop_sniffing_action) +# Initially hide the stop button +stop_button.pack_forget() + +root.mainloop() diff --git a/GUI/port_scan.py b/GUI/port_scan.py new file mode 100644 index 0000000..0bf53af --- /dev/null +++ b/GUI/port_scan.py @@ -0,0 +1,92 @@ +import socket + +# List of common ports to scan +common_ports = { + 20: "FTP-Data", + 21: "FTP", + 22: "SSH", + 23: "Telnet", + 25: "SMTP", + 53: "DNS", + 67: "DHCP", + 68: "DHCP", + 69: "TFTP", + 80: "HTTP", + 110: "POP3", + 119: "NNTP", + 123: "NTP", + 135: "RPC", + 137: "NetBIOS", + 138: "NetBIOS", + 139: "NetBIOS", + 143: "IMAP", + 161: "SNMP", + 162: "SNMP-Trap", + 179: "BGP", + 194: "IRC", + 201: "AppleTalk", + 443: "HTTPS", + 445: "SMB", + 465: "SMTPS", + 500: "IKE", + 514: "Syslog", + 515: "LPD", + 520: "RIP", + 587: "SMTP-Submission", + 631: "IPP", + 636: "LDAPS", + 993: "IMAPS", + 995: "POP3S", + 1025: "NFS or IIS", + 1026: "Windows-Messenger", + 1027: "Windows-Messenger", + 1433: "MSSQL", + 1434: "MSSQL", + 1723: "PPTP", + 1900: "SSDP", + 2000: "Cisco-SCCP", + 2002: "Globe", + 2049: "NFS", + 2121: "FTP-Proxy", + 3306: "MySQL", + 3389: "RDP", + 3689: "iTunes", + 3690: "Subversion", + 4045: "NFS-Lock", + 5060: "SIP", + 5900: "VNC", + 6000: "X11", + 6667: "IRC", + 8000: "HTTP-Alternative", + 8080: "HTTP-Proxy", + 8443: "HTTPS-Alt", + 8888: "HTTP-Alt", + 25565: "Minecraft" +} + +def scan_ports(ip): + print("Scan started") + open_ports = [] + for port, service in common_ports.items(): + print(f"Scanning {port}({service})") + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) # Timeout of 1 second + result = sock.connect_ex((ip, port)) + if result == 0: + open_ports.append((port, service)) + sock.close() + return open_ports + +def main(): + ip = input("Enter the IP address to scan: ") + print(f"Scanning {ip} for common ports...") + open_ports = scan_ports(ip) + if open_ports: + print(f"Open ports on {ip}:") + for port, service in open_ports: + print(f"Port {port}: {service}") + else: + print(f"No common ports are open on {ip}.") + +if __name__ == "__main__": + main() diff --git a/GUI/scanner.py b/GUI/scanner.py new file mode 100644 index 0000000..96c6f7d --- /dev/null +++ b/GUI/scanner.py @@ -0,0 +1,146 @@ +import time +import requests +import subprocess +from scapy.all import ARP, Ether, srp +import dicttoxml +import json +import socket +import sys + +# Function to get the current time formatted as a string +def time_format(): + return time.strftime("%d-%m-%Y_%H:%M:%S") + +# Function to make an API request and return a device's manufacturer by MAC address +def api_lookup(mac_address: str, vV_switch: bool) -> str: + url = f"https://api.macvendors.com/v1/lookup/{mac_address}" + headers = {"Authorization": "Bearer API_KEY"} + if vV_switch: + print("Header for API lookup created") + api = requests.get(url=url, headers=headers) + if vV_switch: + print("API call made") + if "errors" in api.json(): + if vV_switch: + print("Vendor was not found") + return "Unknown" + else: + if vV_switch: + print("Vendor was found successfully") + return api.json()["data"]["organization_name"] + +# Function to get the router's IP address +def get_router_ip(v_switch: bool, vV_switch: bool) -> str: + get_ip_out = subprocess.run("netstat -rn | grep 'default' | awk '{print $2}'", shell=True, capture_output=True, text=True) + if v_switch: + print("Getting router IP") + ip_out = get_ip_out.stdout + ip_out = ip_out.split("\n") + found = False + for item in ip_out: + try: + if len(item.split(".")) == 4: + ip_out = item + found = True + break + else: + continue + except: + continue + if found: + if vV_switch: + print("Got router IP") + return ip_out + else: + print(f"Unsuccessful automatic router IP finder :(\nResult:\n{ip_out}") + sys.exit() + +# Function to create an ARP table map +def arp_table_mapper(ip_addresses, v_switch: bool, vV_switch: bool) -> dict: + arp_table_map = {} + try: + arp_request = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=ip_addresses) + if v_switch: + print("Requested ARP packet") + responses, _ = srp(arp_request, timeout=2, verbose=False) + if vV_switch: + print("Collected ARP responses") + id_counter = 0 + if v_switch: + print("API calls started") + for _, response in responses: + arp_table_map[id_counter] = { + 'ip': response.psrc, + 'mac_address': response.hwsrc, + 'device_name': get_device_name(response.psrc, vV_switch), + 'vendor': api_lookup(response.hwsrc, vV_switch) + } + id_counter += 1 + if v_switch: + print("API calls done") + except Exception as e: + print("Error retrieving ARP table:", e) + return arp_table_map + +# Function to get the device name from its IP address +def get_device_name(ip_address, vV_switch: bool): + try: + hostname, _, _ = socket.gethostbyaddr(ip_address) + if vV_switch: + print("Hostname for IP", ip_address, "is", hostname) + return hostname + except socket.herror: + if vV_switch: + print("Failed to get hostname for:", ip_address) + return "Unknown" + +# Function to get the subnet mask +def get_sub_mask(v_switch: bool) -> str: + if v_switch: + print("Getting subnet mask..") + def subnet_mask_calculation(subnet_mask): + mask_octets = subnet_mask.split('.') + binary_mask = ''.join(format(int(octet), '08b') for octet in mask_octets) + cidr = sum(bit == '1' for bit in binary_mask) + return cidr + sub_mask = subprocess.run("ipconfig getoption en0 subnet_mask", shell=True, capture_output=True, text=True) + sub_mask = sub_mask.stdout.split("\n")[0] + if v_switch: + print("Subnet mask found successfully!") + return str(subnet_mask_calculation(sub_mask)) + +# Function to save the scan result in the specified format +def save(scan_result: dict, form: str, file_name: str, v_switch: bool): + if v_switch: + print("Saving file..") + if form == "json": + with open(f"{file_name}.json", "w") as file: + json.dump(scan_result, file, indent=4) + elif form == "xml": + with open(f"{file_name}.xml", "w") as file: + xml_data = dicttoxml.dicttoxml(scan_result) + file.write(xml_data.decode('utf-8')) + else: + with open(f"{file_name}.txt", "w") as file: + file.write(str(scan_result)) + if v_switch: + print(f"File saved successfully as a {form} file.") + +# Function to display the scan result +def display_result(mapped_network: dict, save_file: bool, save_type: str, file_name: str, v_switch: bool): + if v_switch: + print("\n") + print("\033[91mID\tIP ADDRESS\tMAC ADDRESS\t\tDEVICE NAME\t\tVENDOR\033[0m") + for identifier, content in mapped_network.items(): + tmp_line = f"{identifier}\t{content['ip']}\t{content['mac_address']}\t{content['device_name']} \t\t{content['vendor']}" + print(tmp_line) + if save_file: + save(scan_result=mapped_network, form=save_type, file_name=file_name, v_switch=v_switch) + +# Main scan function +def scan(v_switch: bool = False, vV_switch: bool = False, save_file: bool = False, save_type: str = "json", file_name: str = time_format()) -> dict: + router_ip = get_router_ip(v_switch, vV_switch) + subnet_mask = get_sub_mask(v_switch) + result = arp_table_mapper(f"{router_ip}/{subnet_mask}", v_switch, vV_switch) + display_result(result, save_file, save_type, file_name, v_switch) + return result diff --git a/GUI/sniffer.py b/GUI/sniffer.py new file mode 100644 index 0000000..a3029c1 --- /dev/null +++ b/GUI/sniffer.py @@ -0,0 +1,44 @@ +from scapy.all import sniff, Ether, get_if_list +from datetime import datetime + +sniffing = False # Global flag to control sniffing +packet_store = {} # Store packets for later retrieval + +def get_interface_list(): + try: + return get_if_list() + except Exception as e: + print(f"Error fetching interface list: {str(e)}") + return [] + +def start_sniffing(iface, process_packet): + global sniffing + sniffing = True + try: + sniff(prn=lambda pkt: process_packet(pkt) if sniffing else None, iface=iface, store=False, stop_filter=lambda x: not sniffing) + except Exception as e: + print(f"Error: {str(e)}") + +def stop_sniffing(): + global sniffing + sniffing = False + +def process_packet(packet, tree_sniffer): + if packet.haslayer(Ether): + pkt_time = datetime.now().strftime("%H:%M:%S") + src_mac = packet[Ether].src + dst_mac = packet[Ether].dst + protocol = packet[Ether].type + + protocol_str = "Unknown" + if protocol == 0x0800: + protocol_str = "IPv4" + elif protocol == 0x0806: + protocol_str = "ARP" + elif protocol == 0x86DD: + protocol_str = "IPv6" + + pkt_id = len(packet_store) # Unique identifier for the packet + packet_store[pkt_id] = packet # Store the packet + + tree_sniffer.insert("", "end", iid=pkt_id, values=(pkt_time, src_mac, dst_mac, protocol_str)) From bf0a9a2242f6b7595ebe61dc87ff1cb457172dfd Mon Sep 17 00:00:00 2001 From: Alex Amygdalios Date: Sat, 14 Jun 2025 17:14:44 +0300 Subject: [PATCH 2/6] Updated README --- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9cb5f89..b341f9c 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,34 @@ ## Overview This Python-based network scanner allows you to scan your local network to identify connected devices, their IP addresses, MAC addresses, device names, and vendors. It utilizes ARP requests to map the network and provides options to save the results in various formats (JSON, XML, or plain text). Additionally, it includes a packet sniffer, a port scanner, and a ping based scanner to find every device on the network. -## Installation -### Prerequisites +## Requirements + - Python 3.x -- Required Python packages: - - `scapy` +- The following Python libraries: + - `tkinter` - `requests` + - `scapy` - `dicttoxml` + - `json` + - `threading` + - `datetime` + - `socket` + - `subprocess` + +## Installation + +1. Clone the repository: + + ```sh + git clone -b App https://github.com/mitzCanCode/netscan + ``` + +2. Install the required Python libraries: + + ```sh + pip install scapy requests dicttoxml + ``` + ### Installing Dependencies @@ -17,13 +38,47 @@ This Python-based network scanner allows you to scan your local network to ident pip install scapy requests dicttoxml ``` -### How to run +### How to run CLI ```bash +cd CLI sudo python3 netscan.py ``` + +### How to run GUI +```bash +cd GUI +sudo python3 app.py +``` +## GUI Usage + +### Network Scan + +1. Open the application. +2. Navigate to the "Network Scan" tab. +3. Click the "Scan network" button to start scanning. +4. Optionally, you can save the scan results by selecting the "Save Scan Results" checkbox and choosing a file format (JSON, XML, TXT). + +### Port Scan + +1. Double-click on a device in the network scan results to view detailed information. +2. In the details window, click the "Run Port Scan" button to scan for open ports on the selected device. + +### Packet Sniffer + +1. Navigate to the "Packet Sniffer" tab. +2. Select a network interface from the dropdown menu. +3. Click "Start Sniffing" to begin capturing packets on the selected interface. +4. Click "Stop Sniffing" to stop packet capture. + ### Note To run the script you must first set an API key at the function api_lookup in the scanner.py file. You can get an API key by creating an account on https://macvendors.com +## Acknowledgements + +- The `scapy` library is used for packet manipulation and network scanning. +- The `requests` library is used for making API calls to retrieve device vendor information. + + ## Capabilities This script includes functions to: - Network Scanning: Scan the local network using ARP requests. @@ -37,7 +92,7 @@ This script includes functions to: Contributions are welcome! Please feel free to submit a Pull Request. ## Contact -For any inquiries or issues, please open an issue on the [GitHub repository](https://github.com/dxmxtrxs/netscan). +For any inquiries or issues, please open an issue on the [GitHub repository](https://github.com/mitzCanCode/netscan). --- From 333ffb8393057365dbadbac6169d699afa2765e7 Mon Sep 17 00:00:00 2001 From: Alex Amygdalios Date: Sat, 14 Jun 2025 17:47:20 +0300 Subject: [PATCH 3/6] Added OS Detectio(guessing) to CLI version --- .gitignore | 2 ++ CLI/netscan.py | 33 ++++++++++++++++++++++++++++++++- CLI/os_detection.py | 38 ++++++++++++++++++++++++++++++++++++++ README.md | 1 + 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CLI/os_detection.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c34d28b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +.DS_Store \ No newline at end of file diff --git a/CLI/netscan.py b/CLI/netscan.py index d7886d2..fea52d3 100644 --- a/CLI/netscan.py +++ b/CLI/netscan.py @@ -5,6 +5,7 @@ from quick_scanner import quick_scan # Importing the quick_scan function from the quick_scanner module import os # Importing os module. Used to check if the user is running the script as root. from port_scan import scan_ports +from os_detection import detect_os from datetime import datetime # Importing datetime module for date and time manipulation @@ -57,7 +58,8 @@ def main(): print("4. sniff\t- Sniff packets in the network") print("5. check\t- Run a port scan on a device discovered during a scan") print("6. help\t\t- Display help information about commands or general usage") - print("7. exit\t\t- Exit the program") + print("7. osdetect\t- Detect the potential OS a Host is using.") + print("8. exit\t\t- Exit the program") print("For more inforamtion on a command use: [COMMAND] -h") print("\n") # Printing a new line @@ -75,6 +77,35 @@ def main(): if "exit" in prompt: # If user wants to exit print("\033[91mQuitting...\033[0m ") # Display quitting message sys.exit() # Exit the program + elif "osdetect" in prompt: + if "-h" in prompt: + print("\033[91mOS Detection Help:\033[0m") + print("Usage: osdetect [IP] or [ID]") + print("Description: Attempt to detect the operating system of a host using TTL and TCP fingerprinting.") + print("Examples:") + print(" osdetect 192.168.1.1") + print(" osdetect 3 (ID from previous scan)") + continue + + try: + target = prompt[1] + if target.isdigit() and scan_result and int(target) in scan_result: + ip = scan_result[int(target)]["ip"] + else: + ip = target + + print(f"Detecting OS for {ip}...") + result = detect_os(ip) + if "error" in result: + print(f"Error: {result['error']}") + else: + print(f"IP: {result['ip']}") + print(f"TTL: {result['ttl']}") + print(f"TCP Window Size: {result['tcp_window']}") + print(f"Guessed OS: {result['os_guess']}") + except Exception as e: + print("Usage: osdetect [IP] or [ID from scan result]") + print("Error:", str(e)) elif "help" in prompt: # If user wants help if "-h" in prompt: # If user wants specific help # Printing help message for help command diff --git a/CLI/os_detection.py b/CLI/os_detection.py new file mode 100644 index 0000000..25e8d4b --- /dev/null +++ b/CLI/os_detection.py @@ -0,0 +1,38 @@ +from scapy.all import IP, TCP, sr1, ICMP + +def detect_os(ip): + try: + # Try TCP SYN to port 80 + pkt = IP(dst=ip)/TCP(dport=80, flags="S") + ans = sr1(pkt, timeout=2, verbose=0) + + if not ans: + # Fallback to ICMP + icmp_pkt = IP(dst=ip)/ICMP() + ans = sr1(icmp_pkt, timeout=2, verbose=0) + + if ans: + ttl = int(ans.ttl) + window = ans.getlayer(TCP).window if ans.haslayer(TCP) else None + + # Rough OS guess based on TTL + if ttl <= 64: + os = "Linux/Unix" + elif ttl <= 128: + os = "Windows" + elif ttl <= 255: + os = "Cisco/Network Device" + else: + os = "Unknown" + + return { + "ip": ip, + "ttl": ttl, + "tcp_window": window, + "os_guess": os + } + else: + return {"ip": ip, "error": "No response from host"} + + except Exception as e: + return {"ip": ip, "error": str(e)} \ No newline at end of file diff --git a/README.md b/README.md index b341f9c..538f658 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ This script includes functions to: - Packet Sniffing: Sniff packets on the network. - Ping Scan: Scan by pinging each device on the network. - Port Scanning: Scan for open ports on devices in the network. +- OS Detect: Detect the potential OS of a host ## Contribution Contributions are welcome! Please feel free to submit a Pull Request. From 071c6793e82bee594e35d6c1101b1e7833749fbc Mon Sep 17 00:00:00 2001 From: Alex Amygdalios Date: Sat, 14 Jun 2025 18:01:09 +0300 Subject: [PATCH 4/6] Updated help message to include osdetect --- CLI/netscan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/CLI/netscan.py b/CLI/netscan.py index fea52d3..bb73cb1 100644 --- a/CLI/netscan.py +++ b/CLI/netscan.py @@ -122,6 +122,7 @@ def main(): print("3. results\t- Display the results of the network scan") print("4. sniff\t- Sniff packets in the network") print("5. help\t\t- Display help information about commands or general usage") + print("7. osdetect\t- Detect the potential OS a Host is using.") print("6. exit\t\t- Exit the program") print("For more inforamtion on a command use: [COMMAND] -h") elif "sniff" in prompt: From 3af70d6e3fc669b063a3741d11e3b2bfc5e77e14 Mon Sep 17 00:00:00 2001 From: Alex Amygdalios Date: Fri, 18 Jul 2025 11:58:53 +0300 Subject: [PATCH 5/6] create an actual .env for the API key, make venv, update requirements.txt, update readme --- .gitignore | 4 +++- CLI/scanner.py | 9 ++++++++- GUI/scanner.py | 9 ++++++++- README.md | 18 ++++++++++++++++-- dotenv.template | 1 + requirements.txt | 12 +++++++++--- 6 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 dotenv.template diff --git a/.gitignore b/.gitignore index c34d28b..119b8eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ __pycache__ -.DS_Store \ No newline at end of file +.DS_Store +venv +.env \ No newline at end of file diff --git a/CLI/scanner.py b/CLI/scanner.py index 96c6f7d..8ed1d0e 100644 --- a/CLI/scanner.py +++ b/CLI/scanner.py @@ -6,6 +6,10 @@ import json import socket import sys +import os +from dotenv import load_dotenv + +load_dotenv() # Function to get the current time formatted as a string def time_format(): @@ -13,8 +17,11 @@ def time_format(): # Function to make an API request and return a device's manufacturer by MAC address def api_lookup(mac_address: str, vV_switch: bool) -> str: + + api_key = os.getenv("MAC_VENDORS_APIKEY") + url = f"https://api.macvendors.com/v1/lookup/{mac_address}" - headers = {"Authorization": "Bearer API_KEY"} + headers = {"Authorization": f"Bearer {api_key}"} if vV_switch: print("Header for API lookup created") api = requests.get(url=url, headers=headers) diff --git a/GUI/scanner.py b/GUI/scanner.py index 96c6f7d..8ed1d0e 100644 --- a/GUI/scanner.py +++ b/GUI/scanner.py @@ -6,6 +6,10 @@ import json import socket import sys +import os +from dotenv import load_dotenv + +load_dotenv() # Function to get the current time formatted as a string def time_format(): @@ -13,8 +17,11 @@ def time_format(): # Function to make an API request and return a device's manufacturer by MAC address def api_lookup(mac_address: str, vV_switch: bool) -> str: + + api_key = os.getenv("MAC_VENDORS_APIKEY") + url = f"https://api.macvendors.com/v1/lookup/{mac_address}" - headers = {"Authorization": "Bearer API_KEY"} + headers = {"Authorization": f"Bearer {api_key}"} if vV_switch: print("Header for API lookup created") api = requests.get(url=url, headers=headers) diff --git a/README.md b/README.md index 538f658..c97d8c5 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,26 @@ This Python-based network scanner allows you to scan your local network to ident pip install scapy requests dicttoxml ``` - +### Setting up a venv +```bash +python3 -m venv venv +source venv/bin/activate # On windows: venv\Scripts\Activate +``` ### Installing Dependencies ```bash -pip install scapy requests dicttoxml +pip install -r requirements.txt +``` + +### Create .env file +```bash +mv dotenv.template .env +vim .env # or any editor of your choice, do it with notepad idrc. ``` +and fill in your macvendors.com API key + + +## Usage ### How to run CLI ```bash diff --git a/dotenv.template b/dotenv.template new file mode 100644 index 0000000..8baeebe --- /dev/null +++ b/dotenv.template @@ -0,0 +1 @@ +MAC_VENDORS_APIKEY= # Your API key for macvendors.com \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1305168..dbee0bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,9 @@ -dicttoxml -requests -scapy +certifi==2025.7.14 +charset-normalizer==3.4.2 +dicttoxml==1.7.16 +dotenv==0.9.9 +idna==3.10 +python-dotenv==1.1.1 +requests==2.32.4 +scapy==2.6.1 +urllib3==2.5.0 From cf3b04a60ef26be970a582c6fff71fb068917d76 Mon Sep 17 00:00:00 2001 From: Alex Amygdalios Date: Fri, 18 Jul 2025 12:46:22 +0300 Subject: [PATCH 6/6] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c97d8c5..ee8e323 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This Python-based network scanner allows you to scan your local network to ident 1. Clone the repository: ```sh - git clone -b App https://github.com/mitzCanCode/netscan + git clone https://github.com/mitzCanCode/netscan.git ``` 2. Install the required Python libraries: