Add files via upload
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Obtener el directorio donde está el script
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
# Comprobar si se está ejecutando en una terminal
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
# Estamos en una terminal, pedir sudo directamente
|
||||||
|
echo "Iniciando JNC-Scan..."
|
||||||
|
sudo ./venv/bin/python3 main.py
|
||||||
|
else
|
||||||
|
# No estamos en una terminal (doble clic), intentar abrir una
|
||||||
|
if command -v gnome-terminal &> /dev/null; then
|
||||||
|
gnome-terminal -- bash -c "sudo $DIR/venv/bin/python3 main.py; exec bash"
|
||||||
|
elif command -v x-terminal-emulator &> /dev/null; then
|
||||||
|
x-terminal-emulator -e "sudo $DIR/venv/bin/python3 main.py"
|
||||||
|
elif command -v xterm &> /dev/null; then
|
||||||
|
xterm -e "sudo $DIR/venv/bin/python3 main.py"
|
||||||
|
else
|
||||||
|
# Fallback gráfico si no hay terminal detectada (usando pkexec si existe)
|
||||||
|
if command -v pkexec &> /dev/null; then
|
||||||
|
pkexec $DIR/venv/bin/python3 main.py
|
||||||
|
else
|
||||||
|
notify-send "Error" "No se encontró una terminal compatible para pedir contraseña de root."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from src.gui.main_window import MainWindow
|
||||||
|
from src.gui.styles import apply_styles
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Check for root privileges
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
print("This application requires root privileges to manage network interfaces.")
|
||||||
|
print("Please run with sudo.")
|
||||||
|
# In a real GUI app we might want to show a dialog before exiting,
|
||||||
|
# but for now we'll just print to stdout.
|
||||||
|
# We can also attempt to restart with sudo, but that's complex for now.
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Apply premium styles
|
||||||
|
apply_styles(app)
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
PyQt6
|
||||||
|
pyroute2
|
||||||
|
scapy
|
||||||
|
python-nmap
|
||||||
|
netifaces
|
||||||
|
psutil
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,58 @@
|
|||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
|
||||||
|
class DiagnosticsManager:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run_ping(self, host, count=4):
|
||||||
|
"""
|
||||||
|
Executes the ping command.
|
||||||
|
Returns a generator that yields output lines.
|
||||||
|
"""
|
||||||
|
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
||||||
|
command = ['ping', param, str(count), host]
|
||||||
|
|
||||||
|
return self._run_command(command)
|
||||||
|
|
||||||
|
def run_traceroute(self, host):
|
||||||
|
"""
|
||||||
|
Executes the tracepath command (as replacement for traceroute).
|
||||||
|
Returns a generator that yields output lines.
|
||||||
|
"""
|
||||||
|
# Using tracepath as it's more commonly available without root on modern Linux
|
||||||
|
command = ['tracepath', host]
|
||||||
|
|
||||||
|
return self._run_command(command)
|
||||||
|
|
||||||
|
def run_dns_lookup(self, host, query_type='A'):
|
||||||
|
"""
|
||||||
|
Executes the dig command for DNS lookup.
|
||||||
|
Returns a generator that yields output lines.
|
||||||
|
"""
|
||||||
|
command = ['dig', host, query_type]
|
||||||
|
|
||||||
|
return self._run_command(command)
|
||||||
|
|
||||||
|
def _run_command(self, command):
|
||||||
|
"""
|
||||||
|
Helper to run a command and yield its output line by line.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
universal_newlines=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in process.stdout:
|
||||||
|
yield line.strip()
|
||||||
|
|
||||||
|
process.stdout.close()
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
yield f"Error executing command: {str(e)}"
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
import socket
|
||||||
|
import fcntl
|
||||||
|
import struct
|
||||||
|
import psutil
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
from pyroute2 import IPRoute
|
||||||
|
|
||||||
|
class NetworkManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.ipr = IPRoute()
|
||||||
|
|
||||||
|
def list_interfaces(self):
|
||||||
|
"""
|
||||||
|
Returns a list of dictionaries containing interface details.
|
||||||
|
"""
|
||||||
|
interfaces = []
|
||||||
|
|
||||||
|
# Get basic stats using psutil
|
||||||
|
stats = psutil.net_if_stats()
|
||||||
|
addrs = psutil.net_if_addrs()
|
||||||
|
|
||||||
|
# Get more detailed info using pyroute2
|
||||||
|
links = self.ipr.get_links()
|
||||||
|
|
||||||
|
for link in links:
|
||||||
|
if_name = link.get_attr('IFLA_IFNAME')
|
||||||
|
if if_name == 'lo':
|
||||||
|
continue
|
||||||
|
|
||||||
|
if_index = link['index']
|
||||||
|
mac_address = link.get_attr('IFLA_ADDRESS')
|
||||||
|
operstate = link.get_attr('IFLA_OPERSTATE')
|
||||||
|
|
||||||
|
# Get IP addresses for this interface
|
||||||
|
ip_addrs = self.ipr.get_addr(index=if_index)
|
||||||
|
ipv4 = []
|
||||||
|
ipv6 = []
|
||||||
|
|
||||||
|
for addr in ip_addrs:
|
||||||
|
family = addr['family']
|
||||||
|
ip = addr.get_attr('IFA_ADDRESS')
|
||||||
|
prefixlen = addr['prefixlen']
|
||||||
|
|
||||||
|
if family == socket.AF_INET:
|
||||||
|
ipv4.append(f"{ip}/{prefixlen}")
|
||||||
|
elif family == socket.AF_INET6:
|
||||||
|
ipv6.append(f"{ip}/{prefixlen}")
|
||||||
|
|
||||||
|
# Determine type (Wired/Wireless) - heuristic
|
||||||
|
# In a real scenario, we might check /sys/class/net/<iface>/wireless or use iw
|
||||||
|
is_wireless = False
|
||||||
|
try:
|
||||||
|
# Simple check if wireless extension exists
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
fcntl.ioctl(s.fileno(), 0x8B01, struct.pack('256s', if_name.encode()[:15]))
|
||||||
|
is_wireless = True
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
interfaces.append({
|
||||||
|
'name': if_name,
|
||||||
|
'mac': mac_address,
|
||||||
|
'status': operstate,
|
||||||
|
'ipv4': ipv4,
|
||||||
|
'ipv6': ipv6,
|
||||||
|
'type': 'Wireless' if is_wireless else 'Wired',
|
||||||
|
'is_up': operstate == 'UP',
|
||||||
|
'config': self.get_interface_config(if_name) # Add config details
|
||||||
|
})
|
||||||
|
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
def get_interface_config(self, iface_name):
|
||||||
|
"""
|
||||||
|
Uses nmcli to get the current configuration of the interface.
|
||||||
|
Returns a dict with mode (auto/manual), ip, gateway, dns.
|
||||||
|
"""
|
||||||
|
config = {
|
||||||
|
'method': 'unknown',
|
||||||
|
'ip': '',
|
||||||
|
'prefix': '',
|
||||||
|
'gateway': '',
|
||||||
|
'dns': ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Get Connection Method (from connection profile)
|
||||||
|
cmd_conn = ["nmcli", "-t", "-f", "GENERAL.CONNECTION", "device", "show", iface_name]
|
||||||
|
res_conn = subprocess.run(cmd_conn, capture_output=True, text=True)
|
||||||
|
conn_name = res_conn.stdout.strip().split(':')[1] if ':' in res_conn.stdout else res_conn.stdout.strip()
|
||||||
|
|
||||||
|
if conn_name:
|
||||||
|
cmd_method = ["nmcli", "-t", "-f", "ipv4.method", "connection", "show", conn_name]
|
||||||
|
res_method = subprocess.run(cmd_method, capture_output=True, text=True)
|
||||||
|
config['method'] = res_method.stdout.strip().split(':')[1] if ':' in res_method.stdout else res_method.stdout.strip()
|
||||||
|
|
||||||
|
# 2. Get Dynamic/Applied Config (from device show)
|
||||||
|
# Fields: IP4.ADDRESS[1], IP4.GATEWAY, IP4.DNS[1]
|
||||||
|
cmd_dev = ["nmcli", "-t", "-f", "IP4.ADDRESS,IP4.GATEWAY,IP4.DNS", "device", "show", iface_name]
|
||||||
|
result = subprocess.run(cmd_dev, capture_output=True, text=True)
|
||||||
|
|
||||||
|
lines = result.stdout.strip().split('\n')
|
||||||
|
for line in lines:
|
||||||
|
if ':' not in line: continue
|
||||||
|
key, value = line.split(':', 1)
|
||||||
|
value = value.strip()
|
||||||
|
|
||||||
|
if key.startswith("IP4.ADDRESS"):
|
||||||
|
if '/' in value:
|
||||||
|
config['ip'], config['prefix'] = value.split('/')
|
||||||
|
else:
|
||||||
|
config['ip'] = value
|
||||||
|
elif key == "IP4.GATEWAY":
|
||||||
|
config['gateway'] = value
|
||||||
|
elif key.startswith("IP4.DNS"):
|
||||||
|
# Append multiple DNS if needed, for now just take the first or comma separate
|
||||||
|
if config['dns']:
|
||||||
|
config['dns'] += ", " + value
|
||||||
|
else:
|
||||||
|
config['dns'] = value
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def apply_interface_config(self, iface_name, method, ip=None, prefix=None, gateway=None, dns=None):
|
||||||
|
"""
|
||||||
|
Uses nmcli to apply configuration.
|
||||||
|
method: 'auto' (DHCP) or 'manual' (Static)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get connection name
|
||||||
|
cmd = ["nmcli", "-t", "-f", "GENERAL.CONNECTION", "device", "show", iface_name]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
conn_name = result.stdout.strip().split(':')[1] if ':' in result.stdout else result.stdout.strip()
|
||||||
|
|
||||||
|
if not conn_name:
|
||||||
|
# Try to create a new connection if none exists?
|
||||||
|
# For now, assume one exists or fail.
|
||||||
|
return False, "No NetworkManager connection found for this interface."
|
||||||
|
|
||||||
|
cmds = ["nmcli", "connection", "modify", conn_name, "ipv4.method", method]
|
||||||
|
|
||||||
|
if method == 'manual':
|
||||||
|
if not ip or not prefix:
|
||||||
|
return False, "IP and Prefix required for manual mode."
|
||||||
|
cmds.extend(["ipv4.addresses", f"{ip}/{prefix}"])
|
||||||
|
if gateway:
|
||||||
|
cmds.extend(["ipv4.gateway", gateway])
|
||||||
|
if dns:
|
||||||
|
cmds.extend(["ipv4.dns", dns])
|
||||||
|
else:
|
||||||
|
# Clear static settings when switching to auto
|
||||||
|
cmds.extend(["ipv4.addresses", "", "ipv4.gateway", "", "ipv4.dns", ""])
|
||||||
|
|
||||||
|
# Apply changes
|
||||||
|
subprocess.run(cmds, check=True)
|
||||||
|
|
||||||
|
# Bring up the connection to apply
|
||||||
|
subprocess.run(["nmcli", "connection", "up", conn_name], check=True)
|
||||||
|
|
||||||
|
return True, f"Configuration applied to {iface_name}"
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
return False, f"Failed to apply config: {str(e)}"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def set_ip_config(self, iface_name, ip_address, prefix_len, gateway=None):
|
||||||
|
# Legacy method wrapper
|
||||||
|
return self.apply_interface_config(iface_name, 'manual', ip_address, prefix_len, gateway)
|
||||||
|
|
||||||
|
def set_dhcp(self, iface_name):
|
||||||
|
# Legacy method wrapper
|
||||||
|
return self.apply_interface_config(iface_name, 'auto')
|
||||||
|
|
||||||
|
def create_vlan_bridge(self, trunk_iface, vlan_id, access_iface):
|
||||||
|
"""
|
||||||
|
Creates a bridge between a VLAN on the trunk interface and an access interface.
|
||||||
|
1. Create VLAN interface (e.g., eth0.35)
|
||||||
|
2. Create Bridge (e.g., br35)
|
||||||
|
3. Add VLAN interface and Access interface to Bridge
|
||||||
|
4. Bring everything UP
|
||||||
|
"""
|
||||||
|
vlan_iface_name = f"{trunk_iface}.{vlan_id}"
|
||||||
|
bridge_name = f"br{vlan_id}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Create VLAN interface
|
||||||
|
trunk_idx = self.ipr.link_lookup(ifname=trunk_iface)[0]
|
||||||
|
self.ipr.link('add', ifname=vlan_iface_name, kind='vlan',
|
||||||
|
link=trunk_idx, vlan_id=int(vlan_id))
|
||||||
|
|
||||||
|
# 2. Create Bridge
|
||||||
|
self.ipr.link('add', ifname=bridge_name, kind='bridge')
|
||||||
|
|
||||||
|
# 3. Add ports to bridge
|
||||||
|
vlan_idx = self.ipr.link_lookup(ifname=vlan_iface_name)[0]
|
||||||
|
access_idx = self.ipr.link_lookup(ifname=access_iface)[0]
|
||||||
|
bridge_idx = self.ipr.link_lookup(ifname=bridge_name)[0]
|
||||||
|
|
||||||
|
self.ipr.link('set', index=vlan_idx, master=bridge_idx)
|
||||||
|
self.ipr.link('set', index=access_idx, master=bridge_idx)
|
||||||
|
|
||||||
|
# 4. Bring up interfaces
|
||||||
|
self.ipr.link('set', index=vlan_idx, state='up')
|
||||||
|
self.ipr.link('set', index=access_idx, state='up')
|
||||||
|
self.ipr.link('set', index=bridge_idx, state='up')
|
||||||
|
|
||||||
|
return True, f"Bridge {bridge_name} created successfully."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def create_simple_bridge(self, iface1, iface2):
|
||||||
|
"""
|
||||||
|
Creates a simple bridge between two interfaces (Passthrough).
|
||||||
|
"""
|
||||||
|
bridge_name = "br_pass"
|
||||||
|
try:
|
||||||
|
# Check if bridge exists
|
||||||
|
if self.ipr.link_lookup(ifname=bridge_name):
|
||||||
|
return False, f"Bridge {bridge_name} already exists."
|
||||||
|
|
||||||
|
# Create bridge
|
||||||
|
self.ipr.link('add', ifname=bridge_name, kind='bridge')
|
||||||
|
br_idx = self.ipr.link_lookup(ifname=bridge_name)[0]
|
||||||
|
|
||||||
|
# Get indices
|
||||||
|
idx1 = self.ipr.link_lookup(ifname=iface1)[0]
|
||||||
|
idx2 = self.ipr.link_lookup(ifname=iface2)[0]
|
||||||
|
|
||||||
|
# Helper to prepare interface
|
||||||
|
def prepare_interface(iface_name):
|
||||||
|
# Check if wireless
|
||||||
|
is_wireless = False
|
||||||
|
try:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
fcntl.ioctl(s.fileno(), 0x8B01, struct.pack('256s', iface_name.encode()[:15]))
|
||||||
|
is_wireless = True
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if is_wireless:
|
||||||
|
# Try to enable 4addr mode (WDS) for bridging
|
||||||
|
try:
|
||||||
|
subprocess.run(["iw", "dev", iface_name, "set", "4addr", "on"], check=True, capture_output=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
# If 4addr fails, we proceed but warn/expect failure
|
||||||
|
pass
|
||||||
|
except FileNotFoundError:
|
||||||
|
# iw command not installed
|
||||||
|
pass
|
||||||
|
|
||||||
|
prepare_interface(iface1)
|
||||||
|
prepare_interface(iface2)
|
||||||
|
|
||||||
|
# Add ports to bridge
|
||||||
|
try:
|
||||||
|
self.ipr.link('set', index=idx1, master=br_idx)
|
||||||
|
self.ipr.link('set', index=idx2, master=br_idx)
|
||||||
|
except Exception as e:
|
||||||
|
if "95" in str(e) or "Operation not supported" in str(e):
|
||||||
|
# Cleanup
|
||||||
|
self.ipr.link('del', index=br_idx)
|
||||||
|
return False, (f"Bridging failed (Error 95). Wi-Fi interfaces in Client mode cannot be bridged "
|
||||||
|
f"unless they support '4addr' (WDS) mode. Your card/driver may not support this.")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# Bring everything up
|
||||||
|
self.ipr.link('set', index=br_idx, state='up')
|
||||||
|
self.ipr.link('set', index=idx1, state='up')
|
||||||
|
self.ipr.link('set', index=idx2, state='up')
|
||||||
|
|
||||||
|
return True, f"Bridge {bridge_name} created between {iface1} and {iface2}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Cleanup if failed
|
||||||
|
try:
|
||||||
|
br_idx = self.ipr.link_lookup(ifname=bridge_name)
|
||||||
|
if br_idx:
|
||||||
|
self.ipr.link('del', index=br_idx[0])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def start_dhcp_server(self, interface, ip, range_start, range_end):
|
||||||
|
"""
|
||||||
|
Starts a dnsmasq DHCP server on the specified interface.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. Configure IP on the interface
|
||||||
|
self.apply_interface_config(interface, 'manual', ip, '24') # Assuming /24 for simplicity
|
||||||
|
|
||||||
|
# 2. Kill existing dnsmasq for this interface (simple cleanup)
|
||||||
|
subprocess.run(["pkill", "-f", f"dnsmasq.*{interface}"], check=False)
|
||||||
|
|
||||||
|
# 3. Start dnsmasq
|
||||||
|
# --interface=br_pass --dhcp-range=192.168.x.x,192.168.x.y,12h --bind-interfaces
|
||||||
|
cmd = [
|
||||||
|
"dnsmasq",
|
||||||
|
"--no-daemon",
|
||||||
|
f"--interface={interface}",
|
||||||
|
f"--dhcp-range={range_start},{range_end},12h",
|
||||||
|
f"--listen-address={ip}",
|
||||||
|
"--bind-interfaces"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Run in background
|
||||||
|
subprocess.Popen(cmd)
|
||||||
|
|
||||||
|
return True, f"DHCP Server started on {interface}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def delete_bridge(self, bridge_name):
|
||||||
|
try:
|
||||||
|
# Cleanup DHCP if running
|
||||||
|
subprocess.run(["pkill", "-f", f"dnsmasq.*{bridge_name}"], check=False)
|
||||||
|
|
||||||
|
idx = self.ipr.link_lookup(ifname=bridge_name)
|
||||||
|
if not idx:
|
||||||
|
return False, "Bridge not found"
|
||||||
|
|
||||||
|
self.ipr.link('del', index=idx[0])
|
||||||
|
return True, "Bridge deleted"
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def setup_wifi_ap(self, wifi_iface, ssid, password, bridge_name):
|
||||||
|
"""
|
||||||
|
Sets up a Wi-Fi Access Point bridged to the specified bridge.
|
||||||
|
Uses hostapd.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 1. Create hostapd config
|
||||||
|
hostapd_conf = f"""
|
||||||
|
interface={wifi_iface}
|
||||||
|
bridge={bridge_name}
|
||||||
|
driver=nl80211
|
||||||
|
ssid={ssid}
|
||||||
|
hw_mode=g
|
||||||
|
channel=7
|
||||||
|
wmm_enabled=0
|
||||||
|
macaddr_acl=0
|
||||||
|
auth_algs=1
|
||||||
|
ignore_broadcast_ssid=0
|
||||||
|
wpa=2
|
||||||
|
wpa_passphrase={password}
|
||||||
|
wpa_key_mgmt=WPA-PSK
|
||||||
|
wpa_pairwise=TKIP
|
||||||
|
rsn_pairwise=CCMP
|
||||||
|
"""
|
||||||
|
conf_path = f"/tmp/hostapd_{wifi_iface}.conf"
|
||||||
|
with open(conf_path, "w") as f:
|
||||||
|
f.write(hostapd_conf)
|
||||||
|
|
||||||
|
# 2. Kill existing hostapd on this interface
|
||||||
|
subprocess.run(["pkill", "-f", f"hostapd_{wifi_iface}.conf"], check=False)
|
||||||
|
|
||||||
|
# 3. Start hostapd
|
||||||
|
try:
|
||||||
|
subprocess.Popen(["hostapd", conf_path])
|
||||||
|
return True, "Wi-Fi AP started."
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.ipr.close()
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import nmap
|
||||||
|
import subprocess
|
||||||
|
import shlex
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
from PyQt6.QtCore import QThread, pyqtSignal
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
class ScannerThread(QThread):
|
||||||
|
scan_finished = pyqtSignal(dict)
|
||||||
|
scan_error = pyqtSignal(str)
|
||||||
|
scan_progress = pyqtSignal(str)
|
||||||
|
port_found = pyqtSignal(int, str, str, str) # port, proto, state, service
|
||||||
|
|
||||||
|
def __init__(self, target, arguments):
|
||||||
|
super().__init__()
|
||||||
|
self.target = target
|
||||||
|
self.arguments = arguments
|
||||||
|
self.process = None
|
||||||
|
self.temp_xml = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
# Create temp file for XML output
|
||||||
|
fd, self.temp_xml = tempfile.mkstemp(suffix='.xml')
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
# Construct command: nmap -oX <temp_file> -v <arguments> <target>
|
||||||
|
# -v is important to get progress output on stdout
|
||||||
|
cmd = ["nmap", "-oX", self.temp_xml, "-v"] + shlex.split(self.arguments) + [self.target]
|
||||||
|
|
||||||
|
self.scan_progress.emit(f"Executing: {' '.join(cmd)}\n")
|
||||||
|
|
||||||
|
self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
|
||||||
|
|
||||||
|
# Regex for discovered ports: "Discovered open port 80/tcp on 192.168.1.1"
|
||||||
|
port_regex = re.compile(r"Discovered open port (\d+)/(\w+) on")
|
||||||
|
|
||||||
|
# Read stdout line by line for progress
|
||||||
|
while True:
|
||||||
|
line = self.process.stdout.readline()
|
||||||
|
if not line and self.process.poll() is not None:
|
||||||
|
break
|
||||||
|
if line:
|
||||||
|
self.scan_progress.emit(line.strip())
|
||||||
|
# Check for discovered port
|
||||||
|
match = port_regex.search(line)
|
||||||
|
if match:
|
||||||
|
port = int(match.group(1))
|
||||||
|
proto = match.group(2).upper()
|
||||||
|
self.port_found.emit(port, proto, "open", "")
|
||||||
|
|
||||||
|
stdout, stderr = self.process.communicate()
|
||||||
|
|
||||||
|
if self.process.returncode == 0:
|
||||||
|
# Parse XML output from file
|
||||||
|
nm = nmap.PortScanner()
|
||||||
|
try:
|
||||||
|
with open(self.temp_xml, 'r') as f:
|
||||||
|
xml_content = f.read()
|
||||||
|
|
||||||
|
if not xml_content:
|
||||||
|
self.scan_error.emit("Scan produced no output.")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = nm.analyse_nmap_xml_scan(xml_content)
|
||||||
|
if result is None:
|
||||||
|
result = nm.scan_result
|
||||||
|
self.scan_finished.emit(result)
|
||||||
|
except Exception as e:
|
||||||
|
self.scan_error.emit(f"Error parsing scan results: {str(e)}")
|
||||||
|
else:
|
||||||
|
# If killed or failed
|
||||||
|
if self.process.returncode == -9: # SIGKILL
|
||||||
|
self.scan_progress.emit("\nScan stopped by user.")
|
||||||
|
self.scan_error.emit("Scan stopped by user.")
|
||||||
|
else:
|
||||||
|
self.scan_error.emit(f"Scan failed: {stderr}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.scan_error.emit(str(e))
|
||||||
|
finally:
|
||||||
|
# Cleanup temp file
|
||||||
|
if self.temp_xml and os.path.exists(self.temp_xml):
|
||||||
|
try:
|
||||||
|
os.remove(self.temp_xml)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.process:
|
||||||
|
self.process.kill()
|
||||||
|
self.wait()
|
||||||
|
|
||||||
|
class NetworkScanner:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def scan(self, target, ports=None, scan_type="TCP", skip_discovery=False):
|
||||||
|
"""
|
||||||
|
Initiates a scan. Returns the thread object.
|
||||||
|
target: IP or range (e.g., '192.168.1.1' or '192.168.1.0/24')
|
||||||
|
ports: String of ports (e.g., '80,443' or '1-1000')
|
||||||
|
scan_type: 'TCP' or 'UDP'
|
||||||
|
skip_discovery: If True, adds -Pn (treat all as online). If False, performs host discovery (ARP/Ping).
|
||||||
|
"""
|
||||||
|
# -T4: Aggressive timing
|
||||||
|
# --stats-every 5s: Print progress line every 5 seconds
|
||||||
|
# --min-rate 1000: Send packets at least 1000 per second (Speed up)
|
||||||
|
arguments = "-T4 --stats-every 5s --min-rate 1000"
|
||||||
|
|
||||||
|
if skip_discovery:
|
||||||
|
arguments += " -Pn"
|
||||||
|
|
||||||
|
if scan_type == "UDP":
|
||||||
|
arguments += " -sU"
|
||||||
|
else:
|
||||||
|
arguments += " -sT" # Connect scan (safer for non-root)
|
||||||
|
|
||||||
|
if ports:
|
||||||
|
arguments += f" -p {ports}"
|
||||||
|
else:
|
||||||
|
arguments += " -F" # Fast scan if no ports specified
|
||||||
|
|
||||||
|
thread = ScannerThread(target, arguments)
|
||||||
|
thread.start()
|
||||||
|
return thread
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
from scapy.all import sniff, IP, TCP, UDP, ARP, ICMP
|
||||||
|
from PyQt6.QtCore import QThread, pyqtSignal
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
class SnifferThread(QThread):
|
||||||
|
packet_received = pyqtSignal(dict)
|
||||||
|
error_occurred = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, interface, filter_str=""):
|
||||||
|
super().__init__()
|
||||||
|
self.interface = interface
|
||||||
|
self.filter_str = filter_str
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
# Check if interface is up
|
||||||
|
if not self.is_interface_up(self.interface):
|
||||||
|
raise Exception(f"Interface {self.interface} is DOWN. Please enable it first.")
|
||||||
|
|
||||||
|
sniff(iface=self.interface,
|
||||||
|
prn=self.process_packet,
|
||||||
|
filter=self.filter_str,
|
||||||
|
store=0,
|
||||||
|
stop_filter=lambda x: not self.running)
|
||||||
|
except Exception as e:
|
||||||
|
self.error_occurred.emit(str(e))
|
||||||
|
|
||||||
|
def is_interface_up(self, interface):
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
stats = psutil.net_if_stats()
|
||||||
|
if interface in stats:
|
||||||
|
return stats[interface].isup
|
||||||
|
return False
|
||||||
|
except:
|
||||||
|
return True # Assume up if check fails to avoid blocking
|
||||||
|
|
||||||
|
def process_packet(self, packet):
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
pkt_info = {
|
||||||
|
'time': datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3],
|
||||||
|
'src': 'Unknown',
|
||||||
|
'dst': 'Unknown',
|
||||||
|
'proto': 'Unknown',
|
||||||
|
'info': packet.summary()
|
||||||
|
}
|
||||||
|
|
||||||
|
if IP in packet:
|
||||||
|
pkt_info['src'] = packet[IP].src
|
||||||
|
pkt_info['dst'] = packet[IP].dst
|
||||||
|
pkt_info['proto'] = packet[IP].proto
|
||||||
|
|
||||||
|
# Printer Ports
|
||||||
|
printer_ports = [631, 515, 9100]
|
||||||
|
|
||||||
|
if TCP in packet:
|
||||||
|
pkt_info['proto'] = 'TCP'
|
||||||
|
flags = packet[TCP].flags
|
||||||
|
flag_str = str(flags)
|
||||||
|
|
||||||
|
sport = packet[TCP].sport
|
||||||
|
dport = packet[TCP].dport
|
||||||
|
|
||||||
|
info = f"{sport} -> {dport} [{flag_str}]"
|
||||||
|
|
||||||
|
# Check for Printer Traffic
|
||||||
|
if sport in printer_ports or dport in printer_ports:
|
||||||
|
info = f"[PRINTER] {info}"
|
||||||
|
|
||||||
|
# Check for potential issues (simple heuristics)
|
||||||
|
if 'R' in flag_str: # RST
|
||||||
|
info += " [RESET]"
|
||||||
|
pkt_info['status'] = 'Error'
|
||||||
|
elif 'S' in flag_str and 'A' not in flag_str: # SYN only
|
||||||
|
info += " [SYN]"
|
||||||
|
pkt_info['status'] = 'Connecting'
|
||||||
|
else:
|
||||||
|
pkt_info['status'] = 'OK'
|
||||||
|
|
||||||
|
pkt_info['info'] = info
|
||||||
|
|
||||||
|
elif UDP in packet:
|
||||||
|
pkt_info['proto'] = 'UDP'
|
||||||
|
sport = packet[UDP].sport
|
||||||
|
dport = packet[UDP].dport
|
||||||
|
info = f"{sport} -> {dport}"
|
||||||
|
|
||||||
|
if sport in printer_ports or dport in printer_ports:
|
||||||
|
info = f"[PRINTER] {info}"
|
||||||
|
|
||||||
|
pkt_info['info'] = info
|
||||||
|
pkt_info['status'] = 'OK'
|
||||||
|
|
||||||
|
elif ICMP in packet:
|
||||||
|
pkt_info['proto'] = 'ICMP'
|
||||||
|
icmp_type = packet[ICMP].type
|
||||||
|
icmp_code = packet[ICMP].code
|
||||||
|
|
||||||
|
if icmp_type == 3: # Destination Unreachable
|
||||||
|
pkt_info['info'] = f"Dest Unreachable (Code: {icmp_code})"
|
||||||
|
pkt_info['status'] = 'Error'
|
||||||
|
else:
|
||||||
|
pkt_info['info'] = f"Type: {icmp_type} Code: {icmp_code}"
|
||||||
|
pkt_info['status'] = 'OK'
|
||||||
|
|
||||||
|
elif ARP in packet:
|
||||||
|
pkt_info['proto'] = 'ARP'
|
||||||
|
pkt_info['src'] = packet[ARP].psrc
|
||||||
|
pkt_info['dst'] = packet[ARP].pdst
|
||||||
|
pkt_info['info'] = f"Who has {packet[ARP].pdst}? Tell {packet[ARP].psrc}"
|
||||||
|
pkt_info['status'] = 'OK'
|
||||||
|
|
||||||
|
self.packet_received.emit(pkt_info)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
class NetworkSniffer:
|
||||||
|
def __init__(self):
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def start_sniffing(self, interface, filter_str=""):
|
||||||
|
self.thread = SnifferThread(interface, filter_str)
|
||||||
|
self.thread.start()
|
||||||
|
return self.thread
|
||||||
|
|
||||||
|
def stop_sniffing(self):
|
||||||
|
if self.thread:
|
||||||
|
self.thread.stop()
|
||||||
|
self.thread.wait()
|
||||||
|
self.thread = None
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
from PyQt6.QtCore import QThread, pyqtSignal
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
class VLANScannerThread(QThread):
|
||||||
|
vlan_detected = pyqtSignal(int, int) # vlan_id, count
|
||||||
|
untagged_detected = pyqtSignal(int) # count
|
||||||
|
scan_finished = pyqtSignal()
|
||||||
|
error_occurred = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, interface, timeout=10):
|
||||||
|
super().__init__()
|
||||||
|
self.interface = interface
|
||||||
|
self.timeout = timeout
|
||||||
|
self.running = True
|
||||||
|
self.process = None
|
||||||
|
self.detected_vlans = {} # vlan_id -> count
|
||||||
|
self.untagged_count = 0
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
# Use tcpdump to capture ALL packets to detect untagged vs tagged
|
||||||
|
# -i: interface
|
||||||
|
# -e: print link-level header (to see vlan)
|
||||||
|
# -n: don't resolve names
|
||||||
|
# -l: buffered output
|
||||||
|
# not arp and not stp: filter out some noise if desired, but let's keep it simple
|
||||||
|
cmd = ["tcpdump", "-i", self.interface, "-e", "-n", "-l"]
|
||||||
|
|
||||||
|
self.process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
text=True,
|
||||||
|
bufsize=1
|
||||||
|
)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
# Check timeout
|
||||||
|
if time.time() - start_time > self.timeout:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Non-blocking read line
|
||||||
|
line = self.process.stdout.readline()
|
||||||
|
if not line and self.process.poll() is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
if line:
|
||||||
|
self.parse_line(line)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.error_occurred.emit(str(e))
|
||||||
|
finally:
|
||||||
|
if self.process:
|
||||||
|
self.process.terminate()
|
||||||
|
try:
|
||||||
|
self.process.wait(timeout=1)
|
||||||
|
except:
|
||||||
|
self.process.kill()
|
||||||
|
self.scan_finished.emit()
|
||||||
|
|
||||||
|
def parse_line(self, line):
|
||||||
|
# Example Tagged: ... ethertype 802.1Q (0x8100), length 100: vlan 35, p 0, ethertype IPv4 ...
|
||||||
|
# Example Untagged: ... ethertype IPv4 (0x0800), length 100: ...
|
||||||
|
|
||||||
|
# Regex to find "vlan <id>"
|
||||||
|
match = re.search(r"vlan (\d+)", line)
|
||||||
|
if match:
|
||||||
|
vlan_id = int(match.group(1))
|
||||||
|
if vlan_id not in self.detected_vlans:
|
||||||
|
self.detected_vlans[vlan_id] = 0
|
||||||
|
|
||||||
|
self.detected_vlans[vlan_id] += 1
|
||||||
|
self.vlan_detected.emit(vlan_id, self.detected_vlans[vlan_id])
|
||||||
|
else:
|
||||||
|
# It's an untagged packet (or at least not 802.1Q)
|
||||||
|
# Filter out empty lines or weird output
|
||||||
|
if "ethertype" in line:
|
||||||
|
self.untagged_count += 1
|
||||||
|
self.untagged_detected.emit(self.untagged_count)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
if self.process:
|
||||||
|
self.process.kill() # Force kill to break readline()
|
||||||
|
self.wait()
|
||||||
|
|
||||||
|
class VLANScanner:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def scan(self, interface, timeout=10):
|
||||||
|
thread = VLANScannerThread(interface, timeout)
|
||||||
|
thread.start()
|
||||||
|
return thread
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,129 @@
|
|||||||
|
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QTabWidget,
|
||||||
|
QLabel, QHBoxLayout, QStatusBar)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from src.core.network_manager import NetworkManager
|
||||||
|
from src.gui.widgets.interface_list import InterfaceListWidget
|
||||||
|
from src.gui.widgets.ip_config import IPConfigWidget
|
||||||
|
from src.gui.widgets.vlan_bridge import VLANBridgeWidget
|
||||||
|
from src.gui.widgets.scanner_view import ScannerWidget
|
||||||
|
from src.gui.widgets.sniffer_view import SnifferWidget
|
||||||
|
from src.gui.widgets.diagnostics_view import DiagnosticsWidget
|
||||||
|
from src.gui.widgets.port_scanner import PortScannerWidget
|
||||||
|
from src.utils.report_generator import ReportGenerator
|
||||||
|
from PyQt6.QtWidgets import QFileDialog
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("JNC-NetTools // ADA version")
|
||||||
|
self.setMinimumSize(1000, 700) # Allow resizing, set minimum size
|
||||||
|
|
||||||
|
self.nm = NetworkManager()
|
||||||
|
|
||||||
|
# Central Widget
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
main_layout = QHBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# Sidebar (Interface List)
|
||||||
|
self.interface_list = InterfaceListWidget(self.nm)
|
||||||
|
self.interface_list.setFixedWidth(300)
|
||||||
|
self.interface_list.interface_selected.connect(self.on_interface_selected)
|
||||||
|
main_layout.addWidget(self.interface_list)
|
||||||
|
|
||||||
|
# Main Content Area (Tabs)
|
||||||
|
self.tabs = QTabWidget()
|
||||||
|
main_layout.addWidget(self.tabs)
|
||||||
|
|
||||||
|
# IP Configuration Tab
|
||||||
|
self.ip_config_widget = IPConfigWidget(self.nm)
|
||||||
|
self.tabs.addTab(self.ip_config_widget, "IP Configuration")
|
||||||
|
|
||||||
|
# VLAN & Bridge Tab
|
||||||
|
self.vlan_bridge_widget = VLANBridgeWidget(self.nm)
|
||||||
|
self.tabs.addTab(self.vlan_bridge_widget, "VLAN / Bridge")
|
||||||
|
|
||||||
|
# Scanner Tab (Network Discovery)
|
||||||
|
self.scanner_widget = ScannerWidget()
|
||||||
|
self.tabs.addTab(self.scanner_widget, "Net Scanner")
|
||||||
|
|
||||||
|
# Port Scanner Tab (Dedicated)
|
||||||
|
self.port_scanner_widget = PortScannerWidget()
|
||||||
|
self.tabs.addTab(self.port_scanner_widget, "Port Scanner")
|
||||||
|
|
||||||
|
# Sniffer Tab
|
||||||
|
self.sniffer_widget = SnifferWidget(self.nm)
|
||||||
|
self.tabs.addTab(self.sniffer_widget, "Sniffer")
|
||||||
|
|
||||||
|
# Diagnostics Tab
|
||||||
|
self.diagnostics_widget = DiagnosticsWidget()
|
||||||
|
self.tabs.addTab(self.diagnostics_widget, "Diagnostics")
|
||||||
|
|
||||||
|
# Report Button (Top Right or Bottom)
|
||||||
|
# Let's add a toolbar for global actions
|
||||||
|
toolbar = self.addToolBar("Main")
|
||||||
|
toolbar.addAction("Generate Report", self.generate_report)
|
||||||
|
|
||||||
|
# Status Bar
|
||||||
|
self.status_bar = QStatusBar()
|
||||||
|
self.setStatusBar(self.status_bar)
|
||||||
|
self.status_bar.showMessage("Ready")
|
||||||
|
|
||||||
|
def on_interface_selected(self, iface_data):
|
||||||
|
self.status_bar.showMessage(f"Selected Interface: {iface_data['name']}")
|
||||||
|
self.ip_config_widget.set_interface(iface_data)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
self.nm.close()
|
||||||
|
self.scanner_widget.close()
|
||||||
|
self.port_scanner_widget.close()
|
||||||
|
self.vlan_bridge_widget.close()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def generate_report(self):
|
||||||
|
# Gather data
|
||||||
|
interfaces = self.nm.list_interfaces()
|
||||||
|
|
||||||
|
# Get scan results from scanner widget
|
||||||
|
scan_results = []
|
||||||
|
for row in range(self.scanner_widget.table.rowCount()):
|
||||||
|
scan_results.append({
|
||||||
|
'ip': self.scanner_widget.table.item(row, 0).text(),
|
||||||
|
'mac': self.scanner_widget.table.item(row, 1).text(),
|
||||||
|
'vendor': self.scanner_widget.table.item(row, 2).text(),
|
||||||
|
'status': self.scanner_widget.table.item(row, 3).text()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get sniffer results (All Traffic)
|
||||||
|
sniff_all = []
|
||||||
|
for row in range(self.sniffer_widget.table_all.rowCount()):
|
||||||
|
sniff_all.append({
|
||||||
|
'time': self.sniffer_widget.table_all.item(row, 0).text(),
|
||||||
|
'src': self.sniffer_widget.table_all.item(row, 1).text(),
|
||||||
|
'dst': self.sniffer_widget.table_all.item(row, 2).text(),
|
||||||
|
'proto': self.sniffer_widget.table_all.item(row, 3).text(),
|
||||||
|
'info': self.sniffer_widget.table_all.item(row, 4).text(),
|
||||||
|
'status': self.sniffer_widget.table_all.item(row, 5).text()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get sniffer results (Failed/Anomalies)
|
||||||
|
sniff_failed = []
|
||||||
|
for row in range(self.sniffer_widget.table_failed.rowCount()):
|
||||||
|
sniff_failed.append({
|
||||||
|
'time': self.sniffer_widget.table_failed.item(row, 0).text(),
|
||||||
|
'src': self.sniffer_widget.table_failed.item(row, 1).text(),
|
||||||
|
'dst': self.sniffer_widget.table_failed.item(row, 2).text(),
|
||||||
|
'proto': self.sniffer_widget.table_failed.item(row, 3).text(),
|
||||||
|
'info': self.sniffer_widget.table_failed.item(row, 4).text(),
|
||||||
|
'status': self.sniffer_widget.table_failed.item(row, 5).text()
|
||||||
|
})
|
||||||
|
|
||||||
|
generator = ReportGenerator()
|
||||||
|
filename, _ = QFileDialog.getSaveFileName(self, "Save Report", "report.html", "HTML Files (*.html)")
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
success, msg = generator.generate_html_report(interfaces, scan_results, sniff_all, sniff_failed, filename)
|
||||||
|
if success:
|
||||||
|
self.status_bar.showMessage(f"Report saved to {msg}")
|
||||||
|
else:
|
||||||
|
self.status_bar.showMessage(f"Error saving report: {msg}")
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from PyQt6.QtGui import QPalette, QColor
|
||||||
|
|
||||||
|
def apply_styles(app: QApplication):
|
||||||
|
"""Applies a premium dark theme to the application."""
|
||||||
|
app.setStyle("Fusion")
|
||||||
|
|
||||||
|
palette = QPalette()
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
dark_bg = QColor(30, 30, 30)
|
||||||
|
darker_bg = QColor(20, 20, 20)
|
||||||
|
text_color = QColor(230, 230, 230)
|
||||||
|
accent_color = QColor(0, 120, 215) # A nice blue
|
||||||
|
accent_hover = QColor(0, 140, 240)
|
||||||
|
disabled_text = QColor(127, 127, 127)
|
||||||
|
|
||||||
|
palette.setColor(QPalette.ColorRole.Window, dark_bg)
|
||||||
|
palette.setColor(QPalette.ColorRole.WindowText, text_color)
|
||||||
|
palette.setColor(QPalette.ColorRole.Base, darker_bg)
|
||||||
|
palette.setColor(QPalette.ColorRole.AlternateBase, dark_bg)
|
||||||
|
palette.setColor(QPalette.ColorRole.ToolTipBase, text_color)
|
||||||
|
palette.setColor(QPalette.ColorRole.ToolTipText, text_color)
|
||||||
|
palette.setColor(QPalette.ColorRole.Text, text_color)
|
||||||
|
palette.setColor(QPalette.ColorRole.Button, dark_bg)
|
||||||
|
palette.setColor(QPalette.ColorRole.ButtonText, text_color)
|
||||||
|
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
|
||||||
|
palette.setColor(QPalette.ColorRole.Link, accent_color)
|
||||||
|
palette.setColor(QPalette.ColorRole.Highlight, accent_color)
|
||||||
|
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black)
|
||||||
|
|
||||||
|
app.setPalette(palette)
|
||||||
|
|
||||||
|
# Stylesheet for specific widgets to add that "premium" feel
|
||||||
|
# Stylesheet for specific widgets to add that "premium" feel
|
||||||
|
app.setStyleSheet("""
|
||||||
|
QMainWindow {
|
||||||
|
background-color: #181818;
|
||||||
|
}
|
||||||
|
QWidget {
|
||||||
|
font-family: 'Segoe UI', 'Roboto', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
QTabWidget::pane {
|
||||||
|
border: 1px solid #333333;
|
||||||
|
background: #252526;
|
||||||
|
border-radius: 4px;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
QTabBar::tab {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #999999;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
margin-right: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
QTabBar::tab:selected {
|
||||||
|
background: #252526;
|
||||||
|
color: #ffffff;
|
||||||
|
border-top: 2px solid #007acc;
|
||||||
|
border-bottom: 1px solid #252526;
|
||||||
|
}
|
||||||
|
QTabBar::tab:hover {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
QPushButton {
|
||||||
|
background-color: #007acc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #0062a3;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #004c80;
|
||||||
|
}
|
||||||
|
QPushButton:disabled {
|
||||||
|
background-color: #333333;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
QLineEdit, QComboBox, QSpinBox {
|
||||||
|
background-color: #333333;
|
||||||
|
border: 1px solid #444444;
|
||||||
|
color: #f0f0f0;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
QLineEdit:focus, QComboBox:focus, QSpinBox:focus {
|
||||||
|
border: 1px solid #007acc;
|
||||||
|
background-color: #3c3c3c;
|
||||||
|
}
|
||||||
|
QTableWidget {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
border: 1px solid #333333;
|
||||||
|
border-radius: 4px;
|
||||||
|
gridline-color: #333333;
|
||||||
|
selection-background-color: #264f78;
|
||||||
|
selection-color: white;
|
||||||
|
alternate-background-color: #252526;
|
||||||
|
}
|
||||||
|
QHeaderView::section {
|
||||||
|
background-color: #252526;
|
||||||
|
color: #cccccc;
|
||||||
|
padding: 6px;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid #333333;
|
||||||
|
border-bottom: 1px solid #333333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QListWidget {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
border: 1px solid #333333;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QListWidget::item {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #252526;
|
||||||
|
}
|
||||||
|
QListWidget::item:selected {
|
||||||
|
background-color: #37373d;
|
||||||
|
color: white;
|
||||||
|
border-left: 2px solid #007acc;
|
||||||
|
}
|
||||||
|
QGroupBox {
|
||||||
|
border: 1px solid #444444;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QGroupBox::title {
|
||||||
|
subcontrol-origin: margin;
|
||||||
|
subcontrol-position: top left;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #007acc;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
QStatusBar {
|
||||||
|
background-color: #007acc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
QProgressBar {
|
||||||
|
border: 1px solid #444444;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background-color: #007acc;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
QSplitter::handle {
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,182 @@
|
|||||||
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QLineEdit, QPushButton, QTextEdit, QComboBox,
|
||||||
|
QTabWidget, QSpinBox, QGroupBox)
|
||||||
|
from PyQt6.QtCore import QThread, pyqtSignal, Qt
|
||||||
|
from src.core.diagnostics import DiagnosticsManager
|
||||||
|
|
||||||
|
class DiagnosticsWorker(QThread):
|
||||||
|
output_signal = pyqtSignal(str)
|
||||||
|
finished_signal = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, func, *args):
|
||||||
|
super().__init__()
|
||||||
|
self.func = func
|
||||||
|
self.args = args
|
||||||
|
self.is_running = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
for line in self.func(*self.args):
|
||||||
|
if not self.is_running:
|
||||||
|
break
|
||||||
|
self.output_signal.emit(line)
|
||||||
|
except Exception as e:
|
||||||
|
self.output_signal.emit(f"Error: {str(e)}")
|
||||||
|
finally:
|
||||||
|
self.finished_signal.emit()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
class DiagnosticsWidget(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.dm = DiagnosticsManager()
|
||||||
|
self.worker = None
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Tabs for different tools
|
||||||
|
self.tabs = QTabWidget()
|
||||||
|
layout.addWidget(self.tabs)
|
||||||
|
|
||||||
|
# Ping Tab
|
||||||
|
self.ping_tab = QWidget()
|
||||||
|
self.init_ping_tab()
|
||||||
|
self.tabs.addTab(self.ping_tab, "Ping")
|
||||||
|
|
||||||
|
# Traceroute Tab
|
||||||
|
self.traceroute_tab = QWidget()
|
||||||
|
self.init_traceroute_tab()
|
||||||
|
self.tabs.addTab(self.traceroute_tab, "Traceroute")
|
||||||
|
|
||||||
|
# DNS Tab
|
||||||
|
self.dns_tab = QWidget()
|
||||||
|
self.init_dns_tab()
|
||||||
|
self.tabs.addTab(self.dns_tab, "DNS Lookup")
|
||||||
|
|
||||||
|
def init_ping_tab(self):
|
||||||
|
layout = QVBoxLayout(self.ping_tab)
|
||||||
|
|
||||||
|
# Controls
|
||||||
|
controls_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
controls_layout.addWidget(QLabel("Host:"))
|
||||||
|
self.ping_host_input = QLineEdit()
|
||||||
|
self.ping_host_input.setPlaceholderText("e.g., google.com or 8.8.8.8")
|
||||||
|
controls_layout.addWidget(self.ping_host_input)
|
||||||
|
|
||||||
|
controls_layout.addWidget(QLabel("Count:"))
|
||||||
|
self.ping_count_input = QSpinBox()
|
||||||
|
self.ping_count_input.setRange(1, 100)
|
||||||
|
self.ping_count_input.setValue(4)
|
||||||
|
controls_layout.addWidget(self.ping_count_input)
|
||||||
|
|
||||||
|
self.ping_btn = QPushButton("Ping")
|
||||||
|
self.ping_btn.clicked.connect(self.start_ping)
|
||||||
|
controls_layout.addWidget(self.ping_btn)
|
||||||
|
|
||||||
|
layout.addLayout(controls_layout)
|
||||||
|
|
||||||
|
# Output
|
||||||
|
self.ping_output = QTextEdit()
|
||||||
|
self.ping_output.setReadOnly(True)
|
||||||
|
layout.addWidget(self.ping_output)
|
||||||
|
|
||||||
|
def init_traceroute_tab(self):
|
||||||
|
layout = QVBoxLayout(self.traceroute_tab)
|
||||||
|
|
||||||
|
# Controls
|
||||||
|
controls_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
controls_layout.addWidget(QLabel("Host:"))
|
||||||
|
self.trace_host_input = QLineEdit()
|
||||||
|
self.trace_host_input.setPlaceholderText("e.g., google.com")
|
||||||
|
controls_layout.addWidget(self.trace_host_input)
|
||||||
|
|
||||||
|
self.trace_btn = QPushButton("Trace")
|
||||||
|
self.trace_btn.clicked.connect(self.start_traceroute)
|
||||||
|
controls_layout.addWidget(self.trace_btn)
|
||||||
|
|
||||||
|
layout.addLayout(controls_layout)
|
||||||
|
|
||||||
|
# Output
|
||||||
|
self.trace_output = QTextEdit()
|
||||||
|
self.trace_output.setReadOnly(True)
|
||||||
|
layout.addWidget(self.trace_output)
|
||||||
|
|
||||||
|
def init_dns_tab(self):
|
||||||
|
layout = QVBoxLayout(self.dns_tab)
|
||||||
|
|
||||||
|
# Controls
|
||||||
|
controls_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
controls_layout.addWidget(QLabel("Host:"))
|
||||||
|
self.dns_host_input = QLineEdit()
|
||||||
|
self.dns_host_input.setPlaceholderText("e.g., google.com")
|
||||||
|
controls_layout.addWidget(self.dns_host_input)
|
||||||
|
|
||||||
|
controls_layout.addWidget(QLabel("Type:"))
|
||||||
|
self.dns_type_input = QComboBox()
|
||||||
|
self.dns_type_input.addItems(["A", "AAAA", "MX", "NS", "TXT", "CNAME", "SOA"])
|
||||||
|
controls_layout.addWidget(self.dns_type_input)
|
||||||
|
|
||||||
|
self.dns_btn = QPushButton("Lookup")
|
||||||
|
self.dns_btn.clicked.connect(self.start_dns)
|
||||||
|
controls_layout.addWidget(self.dns_btn)
|
||||||
|
|
||||||
|
layout.addLayout(controls_layout)
|
||||||
|
|
||||||
|
# Output
|
||||||
|
self.dns_output = QTextEdit()
|
||||||
|
self.dns_output.setReadOnly(True)
|
||||||
|
layout.addWidget(self.dns_output)
|
||||||
|
|
||||||
|
def start_ping(self):
|
||||||
|
host = self.ping_host_input.text().strip()
|
||||||
|
if not host:
|
||||||
|
self.ping_output.append("Please enter a host.")
|
||||||
|
return
|
||||||
|
|
||||||
|
count = self.ping_count_input.value()
|
||||||
|
self.ping_output.clear()
|
||||||
|
self.ping_output.append(f"Pinging {host}...")
|
||||||
|
self.ping_btn.setEnabled(False)
|
||||||
|
|
||||||
|
self.worker = DiagnosticsWorker(self.dm.run_ping, host, count)
|
||||||
|
self.worker.output_signal.connect(self.ping_output.append)
|
||||||
|
self.worker.finished_signal.connect(lambda: self.ping_btn.setEnabled(True))
|
||||||
|
self.worker.start()
|
||||||
|
|
||||||
|
def start_traceroute(self):
|
||||||
|
host = self.trace_host_input.text().strip()
|
||||||
|
if not host:
|
||||||
|
self.trace_output.append("Please enter a host.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.trace_output.clear()
|
||||||
|
self.trace_output.append(f"Tracing route to {host}...")
|
||||||
|
self.trace_btn.setEnabled(False)
|
||||||
|
|
||||||
|
self.worker = DiagnosticsWorker(self.dm.run_traceroute, host)
|
||||||
|
self.worker.output_signal.connect(self.trace_output.append)
|
||||||
|
self.worker.finished_signal.connect(lambda: self.trace_btn.setEnabled(True))
|
||||||
|
self.worker.start()
|
||||||
|
|
||||||
|
def start_dns(self):
|
||||||
|
host = self.dns_host_input.text().strip()
|
||||||
|
if not host:
|
||||||
|
self.dns_output.append("Please enter a host.")
|
||||||
|
return
|
||||||
|
|
||||||
|
query_type = self.dns_type_input.currentText()
|
||||||
|
self.dns_output.clear()
|
||||||
|
self.dns_output.append(f"Looking up {query_type} records for {host}...")
|
||||||
|
self.dns_btn.setEnabled(False)
|
||||||
|
|
||||||
|
self.worker = DiagnosticsWorker(self.dm.run_dns_lookup, host, query_type)
|
||||||
|
self.worker.output_signal.connect(self.dns_output.append)
|
||||||
|
self.worker.finished_signal.connect(lambda: self.dns_btn.setEnabled(True))
|
||||||
|
self.worker.start()
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
|
||||||
|
QLabel, QHBoxLayout, QFrame, QPushButton, QStyle)
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QIcon, QColor, QFont
|
||||||
|
|
||||||
|
class InterfaceListWidget(QWidget):
|
||||||
|
interface_selected = pyqtSignal(dict)
|
||||||
|
|
||||||
|
def __init__(self, network_manager):
|
||||||
|
super().__init__()
|
||||||
|
self.nm = network_manager
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
header_layout = QHBoxLayout()
|
||||||
|
title = QLabel("Network Interfaces")
|
||||||
|
title.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
|
||||||
|
header_layout.addWidget(title)
|
||||||
|
header_layout.addStretch()
|
||||||
|
|
||||||
|
# Refresh Button
|
||||||
|
self.refresh_btn = QPushButton()
|
||||||
|
self.refresh_btn.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload))
|
||||||
|
self.refresh_btn.setToolTip("Refresh Interfaces")
|
||||||
|
self.refresh_btn.setFixedSize(30, 30)
|
||||||
|
self.refresh_btn.clicked.connect(self.refresh_interfaces)
|
||||||
|
header_layout.addWidget(self.refresh_btn)
|
||||||
|
|
||||||
|
self.layout.addLayout(header_layout)
|
||||||
|
|
||||||
|
# List
|
||||||
|
self.list_widget = QListWidget()
|
||||||
|
self.list_widget.itemClicked.connect(self.on_item_clicked)
|
||||||
|
self.layout.addWidget(self.list_widget)
|
||||||
|
|
||||||
|
self.refresh_interfaces()
|
||||||
|
|
||||||
|
def refresh_interfaces(self):
|
||||||
|
self.list_widget.clear()
|
||||||
|
interfaces = self.nm.list_interfaces()
|
||||||
|
|
||||||
|
for iface in interfaces:
|
||||||
|
item = QListWidgetItem()
|
||||||
|
|
||||||
|
# Create custom widget for the item
|
||||||
|
item_widget = QWidget()
|
||||||
|
item_layout = QVBoxLayout(item_widget)
|
||||||
|
item_layout.setContentsMargins(5, 5, 5, 5)
|
||||||
|
|
||||||
|
# Top row: Name + Type + Status
|
||||||
|
top_row = QHBoxLayout()
|
||||||
|
name_label = QLabel(iface['name'])
|
||||||
|
name_label.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold))
|
||||||
|
|
||||||
|
type_label = QLabel(iface['type'])
|
||||||
|
type_label.setStyleSheet("color: #aaaaaa; font-size: 10px;")
|
||||||
|
|
||||||
|
status_color = "#4caf50" if iface['is_up'] else "#f44336"
|
||||||
|
status_label = QLabel("● " + iface['status'])
|
||||||
|
status_label.setStyleSheet(f"color: {status_color}; font-weight: bold;")
|
||||||
|
|
||||||
|
# Config Method Badge (DHCP/Static)
|
||||||
|
config_method = iface.get('config', {}).get('method', 'unknown')
|
||||||
|
method_text = "DHCP" if config_method == 'auto' else "Static" if config_method == 'manual' else config_method.upper()
|
||||||
|
method_label = QLabel(f"[{method_text}]")
|
||||||
|
method_label.setStyleSheet("color: #e0e0e0; font-size: 10px; background-color: #3e3e3e; padding: 2px 4px; border-radius: 3px;")
|
||||||
|
|
||||||
|
top_row.addWidget(name_label)
|
||||||
|
top_row.addWidget(type_label)
|
||||||
|
top_row.addWidget(method_label)
|
||||||
|
top_row.addStretch()
|
||||||
|
top_row.addWidget(status_label)
|
||||||
|
|
||||||
|
# Bottom row: IP + MAC
|
||||||
|
bottom_row = QHBoxLayout()
|
||||||
|
|
||||||
|
ip_text = iface['ipv4'][0] if iface['ipv4'] else "No IP"
|
||||||
|
ip_label = QLabel(f"IP: {ip_text}")
|
||||||
|
ip_label.setStyleSheet("color: #0078d7;")
|
||||||
|
|
||||||
|
mac_label = QLabel(f"MAC: {iface['mac']}")
|
||||||
|
mac_label.setStyleSheet("color: #888888;")
|
||||||
|
|
||||||
|
bottom_row.addWidget(ip_label)
|
||||||
|
bottom_row.addStretch()
|
||||||
|
bottom_row.addWidget(mac_label)
|
||||||
|
|
||||||
|
item_layout.addLayout(top_row)
|
||||||
|
item_layout.addLayout(bottom_row)
|
||||||
|
|
||||||
|
item.setSizeHint(item_widget.sizeHint())
|
||||||
|
self.list_widget.addItem(item)
|
||||||
|
self.list_widget.setItemWidget(item, item_widget)
|
||||||
|
|
||||||
|
# Store data in item
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole, iface)
|
||||||
|
|
||||||
|
def on_item_clicked(self, item):
|
||||||
|
iface_data = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
self.interface_selected.emit(iface_data)
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QLineEdit, QPushButton, QRadioButton, QButtonGroup,
|
||||||
|
QGroupBox, QFormLayout, QMessageBox)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
class IPConfigWidget(QWidget):
|
||||||
|
def __init__(self, network_manager):
|
||||||
|
super().__init__()
|
||||||
|
self.nm = network_manager
|
||||||
|
self.current_iface = None
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# IP Configuration Group
|
||||||
|
config_group = QGroupBox("IP Configuration")
|
||||||
|
config_layout = QVBoxLayout(config_group)
|
||||||
|
|
||||||
|
# Mode Selection
|
||||||
|
mode_layout = QHBoxLayout()
|
||||||
|
self.dhcp_radio = QRadioButton("DHCP")
|
||||||
|
self.static_radio = QRadioButton("Static")
|
||||||
|
self.static_radio.setChecked(True)
|
||||||
|
|
||||||
|
self.mode_group = QButtonGroup()
|
||||||
|
self.mode_group.addButton(self.dhcp_radio)
|
||||||
|
self.mode_group.addButton(self.static_radio)
|
||||||
|
|
||||||
|
mode_layout.addWidget(self.dhcp_radio)
|
||||||
|
mode_layout.addWidget(self.static_radio)
|
||||||
|
mode_layout.addStretch()
|
||||||
|
config_layout.addLayout(mode_layout)
|
||||||
|
|
||||||
|
# Static IP Fields
|
||||||
|
self.form_layout = QFormLayout()
|
||||||
|
self.ip_input = QLineEdit()
|
||||||
|
self.cidr_input = QLineEdit()
|
||||||
|
self.cidr_input.setPlaceholderText("24")
|
||||||
|
self.gateway_input = QLineEdit()
|
||||||
|
|
||||||
|
self.form_layout.addRow("IP Address:", self.ip_input)
|
||||||
|
self.form_layout.addRow("CIDR Prefix:", self.cidr_input)
|
||||||
|
self.form_layout.addRow("Gateway:", self.gateway_input)
|
||||||
|
|
||||||
|
# DNS Field
|
||||||
|
self.dns_input = QLineEdit()
|
||||||
|
self.form_layout.addRow("DNS:", self.dns_input)
|
||||||
|
|
||||||
|
config_layout.addLayout(self.form_layout)
|
||||||
|
|
||||||
|
# Apply Button
|
||||||
|
self.apply_btn = QPushButton("Apply Configuration")
|
||||||
|
self.apply_btn.clicked.connect(self.apply_config)
|
||||||
|
config_layout.addWidget(self.apply_btn)
|
||||||
|
|
||||||
|
self.layout.addWidget(config_group)
|
||||||
|
|
||||||
|
# Subnet Calculator Group
|
||||||
|
calc_group = QGroupBox("Subnet Calculator")
|
||||||
|
calc_layout = QVBoxLayout(calc_group)
|
||||||
|
|
||||||
|
calc_form = QFormLayout()
|
||||||
|
self.calc_ip = QLineEdit()
|
||||||
|
self.calc_cidr = QLineEdit()
|
||||||
|
self.calc_cidr.setPlaceholderText("24")
|
||||||
|
self.calc_netmask = QLineEdit()
|
||||||
|
self.calc_netmask.setReadOnly(True)
|
||||||
|
self.calc_network = QLineEdit()
|
||||||
|
self.calc_network.setReadOnly(True)
|
||||||
|
self.calc_broadcast = QLineEdit()
|
||||||
|
self.calc_broadcast.setReadOnly(True)
|
||||||
|
|
||||||
|
calc_form.addRow("IP Address:", self.calc_ip)
|
||||||
|
calc_form.addRow("CIDR:", self.calc_cidr)
|
||||||
|
calc_form.addRow("Netmask:", self.calc_netmask)
|
||||||
|
calc_form.addRow("Network:", self.calc_network)
|
||||||
|
calc_form.addRow("Broadcast:", self.calc_broadcast)
|
||||||
|
|
||||||
|
calc_layout.addLayout(calc_form)
|
||||||
|
|
||||||
|
# Calculate Button (or auto-calc)
|
||||||
|
self.calc_btn = QPushButton("Calculate")
|
||||||
|
self.calc_btn.clicked.connect(self.calculate_subnet)
|
||||||
|
calc_layout.addWidget(self.calc_btn)
|
||||||
|
|
||||||
|
self.layout.addWidget(calc_group)
|
||||||
|
self.layout.addStretch()
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
self.dhcp_radio.toggled.connect(self.toggle_inputs)
|
||||||
|
self.static_radio.toggled.connect(self.toggle_inputs)
|
||||||
|
|
||||||
|
def set_interface(self, iface_data):
|
||||||
|
self.current_iface = iface_data
|
||||||
|
|
||||||
|
# Get real config from NM
|
||||||
|
config = iface_data.get('config', {})
|
||||||
|
method = config.get('method', 'auto')
|
||||||
|
|
||||||
|
if method == 'manual':
|
||||||
|
self.static_radio.setChecked(True)
|
||||||
|
self.ip_input.setText(config.get('ip', ''))
|
||||||
|
self.cidr_input.setText(config.get('prefix', ''))
|
||||||
|
self.gateway_input.setText(config.get('gateway', ''))
|
||||||
|
self.dns_input.setText(config.get('dns', ''))
|
||||||
|
else:
|
||||||
|
self.dhcp_radio.setChecked(True)
|
||||||
|
# Clear fields or show current lease?
|
||||||
|
# For now, let's show current IP even if DHCP
|
||||||
|
if iface_data['ipv4']:
|
||||||
|
ip, prefix = iface_data['ipv4'][0].split('/')
|
||||||
|
self.ip_input.setText(ip)
|
||||||
|
self.cidr_input.setText(prefix)
|
||||||
|
|
||||||
|
# Update calculator
|
||||||
|
self.calc_ip.setText(self.ip_input.text())
|
||||||
|
self.calc_cidr.setText(self.cidr_input.text())
|
||||||
|
self.calculate_subnet()
|
||||||
|
|
||||||
|
def toggle_inputs(self):
|
||||||
|
enabled = self.static_radio.isChecked()
|
||||||
|
self.ip_input.setEnabled(enabled)
|
||||||
|
self.cidr_input.setEnabled(enabled)
|
||||||
|
self.gateway_input.setEnabled(enabled)
|
||||||
|
self.dns_input.setEnabled(enabled)
|
||||||
|
|
||||||
|
def calculate_subnet(self):
|
||||||
|
ip = self.calc_ip.text()
|
||||||
|
cidr = self.calc_cidr.text()
|
||||||
|
|
||||||
|
if not ip or not cidr:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
network = ipaddress.IPv4Interface(f"{ip}/{cidr}")
|
||||||
|
self.calc_netmask.setText(str(network.netmask))
|
||||||
|
self.calc_network.setText(str(network.network))
|
||||||
|
self.calc_broadcast.setText(str(network.network.broadcast_address))
|
||||||
|
except ValueError as e:
|
||||||
|
self.calc_netmask.setText("Invalid IP/CIDR")
|
||||||
|
|
||||||
|
def apply_config(self):
|
||||||
|
if not self.current_iface:
|
||||||
|
QMessageBox.warning(self, "Error", "No interface selected")
|
||||||
|
return
|
||||||
|
|
||||||
|
iface_name = self.current_iface['name']
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.dhcp_radio.isChecked():
|
||||||
|
success, msg = self.nm.apply_interface_config(iface_name, 'auto')
|
||||||
|
else:
|
||||||
|
ip = self.ip_input.text()
|
||||||
|
cidr = self.cidr_input.text()
|
||||||
|
gateway = self.gateway_input.text()
|
||||||
|
dns = self.dns_input.text()
|
||||||
|
success, msg = self.nm.apply_interface_config(iface_name, 'manual', ip, cidr, gateway, dns)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
QMessageBox.information(self, "Success", msg)
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Error", msg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Error", str(e))
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QLineEdit, QPushButton, QComboBox, QTableWidget,
|
||||||
|
QTableWidgetItem, QHeaderView, QMessageBox, QGroupBox, QTextEdit)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from src.core.scanner import NetworkScanner
|
||||||
|
|
||||||
|
class PortScannerWidget(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.scanner = NetworkScanner()
|
||||||
|
self.scan_thread = None
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Controls Group
|
||||||
|
controls_group = QGroupBox("Target Configuration")
|
||||||
|
controls_layout = QVBoxLayout(controls_group)
|
||||||
|
|
||||||
|
# Target IP
|
||||||
|
target_layout = QHBoxLayout()
|
||||||
|
target_layout.addWidget(QLabel("Target IP:"))
|
||||||
|
self.target_input = QLineEdit()
|
||||||
|
self.target_input.setPlaceholderText("e.g., 192.168.1.50")
|
||||||
|
target_layout.addWidget(self.target_input)
|
||||||
|
controls_layout.addLayout(target_layout)
|
||||||
|
|
||||||
|
# Ports
|
||||||
|
ports_layout = QHBoxLayout()
|
||||||
|
ports_layout.addWidget(QLabel("Ports:"))
|
||||||
|
self.ports_input = QLineEdit()
|
||||||
|
self.ports_input.setPlaceholderText("e.g., 80, 443, 1000-2000")
|
||||||
|
ports_layout.addWidget(self.ports_input)
|
||||||
|
controls_layout.addLayout(ports_layout)
|
||||||
|
|
||||||
|
# Protocol & Button
|
||||||
|
action_layout = QHBoxLayout()
|
||||||
|
action_layout.addWidget(QLabel("Protocol:"))
|
||||||
|
self.protocol_combo = QComboBox()
|
||||||
|
self.protocol_combo.addItems(["TCP", "UDP", "Both"])
|
||||||
|
action_layout.addWidget(self.protocol_combo)
|
||||||
|
|
||||||
|
self.scan_btn = QPushButton("Start Port Scan")
|
||||||
|
self.scan_btn.clicked.connect(self.start_scan)
|
||||||
|
action_layout.addWidget(self.scan_btn)
|
||||||
|
|
||||||
|
controls_layout.addLayout(action_layout)
|
||||||
|
|
||||||
|
self.layout.addWidget(controls_group)
|
||||||
|
|
||||||
|
# Filter & Search Group
|
||||||
|
filter_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
filter_layout.addWidget(QLabel("Filter:"))
|
||||||
|
self.filter_combo = QComboBox()
|
||||||
|
self.filter_combo.addItems(["All", "Open", "Closed"])
|
||||||
|
self.filter_combo.currentTextChanged.connect(self.apply_filters)
|
||||||
|
filter_layout.addWidget(self.filter_combo)
|
||||||
|
|
||||||
|
filter_layout.addWidget(QLabel("Search Port:"))
|
||||||
|
self.search_input = QLineEdit()
|
||||||
|
self.search_input.setPlaceholderText("Search port number...")
|
||||||
|
self.search_input.textChanged.connect(self.apply_filters)
|
||||||
|
filter_layout.addWidget(self.search_input)
|
||||||
|
|
||||||
|
self.layout.addLayout(filter_layout)
|
||||||
|
|
||||||
|
# Results Table
|
||||||
|
self.table = QTableWidget()
|
||||||
|
self.table.setColumnCount(4)
|
||||||
|
self.table.setHorizontalHeaderLabels(["Port", "Protocol", "State", "Service"])
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.layout.addWidget(self.table)
|
||||||
|
|
||||||
|
# Progress Log
|
||||||
|
log_group = QGroupBox("Scan Progress")
|
||||||
|
log_layout = QVBoxLayout(log_group)
|
||||||
|
self.log_output = QTextEdit()
|
||||||
|
self.log_output.setReadOnly(True)
|
||||||
|
self.log_output.setMaximumHeight(150)
|
||||||
|
self.log_output.setStyleSheet("background-color: #1e1e1e; color: #00ff00; font-family: monospace;")
|
||||||
|
log_layout.addWidget(self.log_output)
|
||||||
|
self.layout.addWidget(log_group)
|
||||||
|
|
||||||
|
def start_scan(self):
|
||||||
|
if self.scan_btn.text() == "Stop Scan":
|
||||||
|
self.stop_scan()
|
||||||
|
return
|
||||||
|
|
||||||
|
target = self.target_input.text().strip()
|
||||||
|
ports = self.ports_input.text().strip()
|
||||||
|
protocol = self.protocol_combo.currentText()
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
QMessageBox.warning(self, "Error", "Please enter a target IP.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not ports:
|
||||||
|
QMessageBox.warning(self, "Error", "Please enter ports to scan.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.scan_btn.setText("Stop Scan")
|
||||||
|
self.scan_btn.setStyleSheet("background-color: #800000; color: white;") # Red for stop
|
||||||
|
self.table.setRowCount(0)
|
||||||
|
self.log_output.clear()
|
||||||
|
|
||||||
|
self.current_target = target
|
||||||
|
self.current_ports = ports
|
||||||
|
self.chain_udp = False
|
||||||
|
|
||||||
|
if protocol == "Both":
|
||||||
|
self.chain_udp = True
|
||||||
|
self.run_scan(target, ports, "TCP")
|
||||||
|
else:
|
||||||
|
self.run_scan(target, ports, protocol)
|
||||||
|
|
||||||
|
def run_scan(self, target, ports, protocol):
|
||||||
|
self.log_output.append(f"--- Starting {protocol} Scan ---\n")
|
||||||
|
# Port Scanner should use skip_discovery=True (-Pn) to ensure it scans even if ping blocked
|
||||||
|
self.scan_thread = self.scanner.scan(target, ports, protocol, skip_discovery=True)
|
||||||
|
self.scan_thread.scan_finished.connect(self.on_scan_finished)
|
||||||
|
self.scan_thread.scan_error.connect(self.on_scan_error)
|
||||||
|
self.scan_thread.scan_progress.connect(self.on_scan_progress)
|
||||||
|
self.scan_thread.port_found.connect(self.on_port_found)
|
||||||
|
|
||||||
|
def stop_scan(self):
|
||||||
|
self.chain_udp = False # Cancel chain
|
||||||
|
if self.scan_thread:
|
||||||
|
self.scan_thread.stop()
|
||||||
|
self.scan_btn.setEnabled(False)
|
||||||
|
self.scan_btn.setText("Stopping...")
|
||||||
|
|
||||||
|
def on_scan_progress(self, msg):
|
||||||
|
self.log_output.append(msg)
|
||||||
|
sb = self.log_output.verticalScrollBar()
|
||||||
|
sb.setValue(sb.maximum())
|
||||||
|
|
||||||
|
def on_port_found(self, port, proto, state, service):
|
||||||
|
# Check if already exists
|
||||||
|
items = self.table.findItems(str(port), Qt.MatchFlag.MatchExactly)
|
||||||
|
for item in items:
|
||||||
|
if item.column() == 0:
|
||||||
|
row = item.row()
|
||||||
|
if self.table.item(row, 1).text() == proto:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.add_result_row(port, proto, state, service)
|
||||||
|
|
||||||
|
def on_scan_finished(self, result):
|
||||||
|
# 1. Process XML results
|
||||||
|
found_ports = set()
|
||||||
|
if result and 'scan' in result:
|
||||||
|
for ip, data in result['scan'].items():
|
||||||
|
if 'tcp' in data:
|
||||||
|
for port, info in data['tcp'].items():
|
||||||
|
self.update_or_add_row(port, "TCP", info['state'], info.get('name', ''))
|
||||||
|
found_ports.add((int(port), "TCP"))
|
||||||
|
if 'udp' in data:
|
||||||
|
for port, info in data['udp'].items():
|
||||||
|
self.update_or_add_row(port, "UDP", info['state'], info.get('name', ''))
|
||||||
|
found_ports.add((int(port), "UDP"))
|
||||||
|
|
||||||
|
# 2. Chain UDP if needed
|
||||||
|
if self.chain_udp:
|
||||||
|
self.chain_udp = False
|
||||||
|
self.run_scan(self.current_target, self.current_ports, "UDP")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Finalize (only after all scans done)
|
||||||
|
self.finalize_scan(found_ports)
|
||||||
|
|
||||||
|
def finalize_scan(self, found_ports_unused):
|
||||||
|
self.scan_btn.setEnabled(True)
|
||||||
|
self.scan_btn.setText("Start Port Scan")
|
||||||
|
self.scan_btn.setStyleSheet("")
|
||||||
|
|
||||||
|
# Disable updates to prevent freeze
|
||||||
|
self.table.setUpdatesEnabled(False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Build set of ALREADY FOUND ports from the table
|
||||||
|
existing_ports = set()
|
||||||
|
for row in range(self.table.rowCount()):
|
||||||
|
port_item = self.table.item(row, 0)
|
||||||
|
proto_item = self.table.item(row, 1)
|
||||||
|
if port_item and proto_item:
|
||||||
|
existing_ports.add((int(port_item.text()), proto_item.text()))
|
||||||
|
|
||||||
|
# 2. Parse requested ports
|
||||||
|
requested_ports_str = self.ports_input.text().strip()
|
||||||
|
requested_proto = self.protocol_combo.currentText()
|
||||||
|
|
||||||
|
target_ports = set()
|
||||||
|
try:
|
||||||
|
parts = requested_ports_str.split(',')
|
||||||
|
for part in parts:
|
||||||
|
if '-' in part:
|
||||||
|
start, end = map(int, part.split('-'))
|
||||||
|
target_ports.update(range(start, end + 1))
|
||||||
|
else:
|
||||||
|
target_ports.add(int(part))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Determine protocols
|
||||||
|
protos = []
|
||||||
|
if requested_proto == "TCP" or requested_proto == "Both":
|
||||||
|
protos.append("TCP")
|
||||||
|
if requested_proto == "UDP" or requested_proto == "Both":
|
||||||
|
protos.append("UDP")
|
||||||
|
|
||||||
|
# 3. Add closed rows for missing ports
|
||||||
|
# Only show closed ports if the total number of ports is reasonable (< 500)
|
||||||
|
# to prevent UI freezing and overwhelming the user.
|
||||||
|
if len(target_ports) > 500:
|
||||||
|
self.log_output.append(f"\n[INFO] Large range ({len(target_ports)} ports). Hiding closed ports.")
|
||||||
|
else:
|
||||||
|
self.table.setSortingEnabled(False)
|
||||||
|
|
||||||
|
for port in target_ports:
|
||||||
|
for proto in protos:
|
||||||
|
if (port, proto) not in existing_ports:
|
||||||
|
# Pass update_filter=False for bulk addition
|
||||||
|
self.add_result_row(port, proto, "closed", "", update_filter=False)
|
||||||
|
|
||||||
|
self.table.setSortingEnabled(True)
|
||||||
|
|
||||||
|
self.apply_filters() # Apply once at the end
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.table.setUpdatesEnabled(True)
|
||||||
|
|
||||||
|
def row_exists(self, port, proto):
|
||||||
|
# Deprecated / Unused in optimized version but kept for safety if needed elsewhere
|
||||||
|
items = self.table.findItems(str(port), Qt.MatchFlag.MatchExactly)
|
||||||
|
for item in items:
|
||||||
|
if item.column() == 0:
|
||||||
|
if self.table.item(item.row(), 1).text() == proto:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_or_add_row(self, port, proto, state, service):
|
||||||
|
# Find row
|
||||||
|
items = self.table.findItems(str(port), Qt.MatchFlag.MatchExactly)
|
||||||
|
found_row = -1
|
||||||
|
for item in items:
|
||||||
|
if item.column() == 0:
|
||||||
|
if self.table.item(item.row(), 1).text() == proto:
|
||||||
|
found_row = item.row()
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_row >= 0:
|
||||||
|
# Update
|
||||||
|
self.update_row_style(found_row, state)
|
||||||
|
self.table.setItem(found_row, 3, QTableWidgetItem(service))
|
||||||
|
else:
|
||||||
|
# Add
|
||||||
|
self.add_result_row(port, proto, state, service)
|
||||||
|
|
||||||
|
def add_result_row(self, port, proto, state, service, update_filter=True):
|
||||||
|
row = self.table.rowCount()
|
||||||
|
self.table.insertRow(row)
|
||||||
|
self.table.setItem(row, 0, QTableWidgetItem(str(port)))
|
||||||
|
self.table.setItem(row, 1, QTableWidgetItem(proto))
|
||||||
|
|
||||||
|
state_item = QTableWidgetItem(state)
|
||||||
|
self.table.setItem(row, 2, state_item)
|
||||||
|
self.table.setItem(row, 3, QTableWidgetItem(service))
|
||||||
|
|
||||||
|
self.update_row_style(row, state)
|
||||||
|
|
||||||
|
if update_filter:
|
||||||
|
self.apply_filters() # Re-apply filters to new row
|
||||||
|
|
||||||
|
def update_row_style(self, row, state):
|
||||||
|
state_item = self.table.item(row, 2)
|
||||||
|
if state == 'open':
|
||||||
|
state_item.setForeground(Qt.GlobalColor.green)
|
||||||
|
state_item.setText("open")
|
||||||
|
elif state == 'closed':
|
||||||
|
state_item.setForeground(Qt.GlobalColor.red)
|
||||||
|
state_item.setText("closed")
|
||||||
|
else:
|
||||||
|
state_item.setForeground(Qt.GlobalColor.yellow)
|
||||||
|
state_item.setText(state)
|
||||||
|
|
||||||
|
def apply_filters(self):
|
||||||
|
filter_state = self.filter_combo.currentText().lower()
|
||||||
|
search_text = self.search_input.text().strip()
|
||||||
|
|
||||||
|
for i in range(self.table.rowCount()):
|
||||||
|
show = True
|
||||||
|
|
||||||
|
# State Filter
|
||||||
|
state_item = self.table.item(i, 2)
|
||||||
|
state = state_item.text()
|
||||||
|
if filter_state != "all":
|
||||||
|
if filter_state == "open" and state != "open":
|
||||||
|
show = False
|
||||||
|
elif filter_state == "closed" and state != "closed":
|
||||||
|
show = False
|
||||||
|
|
||||||
|
# Search Filter
|
||||||
|
if show and search_text:
|
||||||
|
port_item = self.table.item(i, 0)
|
||||||
|
if search_text not in port_item.text():
|
||||||
|
show = False
|
||||||
|
|
||||||
|
self.table.setRowHidden(i, not show)
|
||||||
|
|
||||||
|
def on_scan_error(self, msg):
|
||||||
|
self.chain_udp = False # Stop chain on error
|
||||||
|
self.scan_btn.setEnabled(True)
|
||||||
|
self.scan_btn.setText("Start Port Scan")
|
||||||
|
self.scan_btn.setStyleSheet("")
|
||||||
|
|
||||||
|
if "kill" in str(msg) or "None" in str(msg) or "stopped by user" in str(msg):
|
||||||
|
return
|
||||||
|
|
||||||
|
QMessageBox.critical(self, "Error", f"Scan failed: {msg}")
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
if self.scan_thread and self.scan_thread.isRunning():
|
||||||
|
self.scan_thread.stop()
|
||||||
|
event.accept()
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
|
||||||
|
QHeaderView, QMessageBox, QProgressBar, QComboBox, QTextEdit, QGroupBox)
|
||||||
|
from src.core.scanner import NetworkScanner
|
||||||
|
|
||||||
|
class ScannerWidget(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.scanner = NetworkScanner()
|
||||||
|
self.scan_thread = None
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Controls
|
||||||
|
controls_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
self.target_input = QLineEdit()
|
||||||
|
self.target_input.setPlaceholderText("Target Network (e.g. 192.168.1.0/24)")
|
||||||
|
|
||||||
|
self.scan_btn = QPushButton("Scan Network")
|
||||||
|
self.scan_btn.clicked.connect(self.start_scan)
|
||||||
|
|
||||||
|
self.filter_combo = QComboBox()
|
||||||
|
self.filter_combo.addItems(["All", "Up", "Down"])
|
||||||
|
self.filter_combo.currentTextChanged.connect(self.apply_filters)
|
||||||
|
|
||||||
|
controls_layout.addWidget(QLabel("Target:"))
|
||||||
|
controls_layout.addWidget(self.target_input)
|
||||||
|
controls_layout.addWidget(QLabel("Filter:"))
|
||||||
|
controls_layout.addWidget(self.filter_combo)
|
||||||
|
controls_layout.addWidget(self.scan_btn)
|
||||||
|
|
||||||
|
self.layout.addLayout(controls_layout)
|
||||||
|
|
||||||
|
# Progress Bar
|
||||||
|
self.progress = QProgressBar()
|
||||||
|
self.progress.setRange(0, 0) # Indeterminate
|
||||||
|
self.progress.hide()
|
||||||
|
self.layout.addWidget(self.progress)
|
||||||
|
|
||||||
|
# Results Table
|
||||||
|
self.table = QTableWidget()
|
||||||
|
self.table.setColumnCount(5)
|
||||||
|
self.table.setHorizontalHeaderLabels(["IP", "MAC", "Vendor", "Status", "Open Ports"])
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.layout.addWidget(self.table)
|
||||||
|
|
||||||
|
# Progress Log
|
||||||
|
log_group = QGroupBox("Scan Progress")
|
||||||
|
log_layout = QVBoxLayout(log_group)
|
||||||
|
self.log_output = QTextEdit()
|
||||||
|
self.log_output.setReadOnly(True)
|
||||||
|
self.log_output.setMaximumHeight(150)
|
||||||
|
self.log_output.setStyleSheet("background-color: #1e1e1e; color: #00ff00; font-family: monospace;")
|
||||||
|
log_layout.addWidget(self.log_output)
|
||||||
|
self.layout.addWidget(log_group)
|
||||||
|
|
||||||
|
def start_scan(self):
|
||||||
|
if self.scan_btn.text() == "Stop Scan":
|
||||||
|
if self.scan_thread:
|
||||||
|
self.scan_thread.stop()
|
||||||
|
self.scan_btn.setEnabled(False)
|
||||||
|
self.scan_btn.setText("Stopping...")
|
||||||
|
return
|
||||||
|
|
||||||
|
target = self.target_input.text()
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
QMessageBox.warning(self, "Error", "Please enter a target IP range.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.scan_btn.setText("Stop Scan")
|
||||||
|
self.scan_btn.setStyleSheet("background-color: #800000; color: white;")
|
||||||
|
self.progress.show()
|
||||||
|
self.table.setRowCount(0)
|
||||||
|
self.log_output.clear()
|
||||||
|
|
||||||
|
# Use default scan (TCP, Fast ports) for discovery
|
||||||
|
# skip_discovery=False ensures we ONLY scan hosts that are UP (ARP/Ping response)
|
||||||
|
self.scan_thread = self.scanner.scan(target, ports=None, scan_type="TCP", skip_discovery=False)
|
||||||
|
self.scan_thread.scan_finished.connect(self.on_scan_finished)
|
||||||
|
self.scan_thread.scan_error.connect(self.on_scan_error)
|
||||||
|
self.scan_thread.scan_progress.connect(self.on_scan_progress)
|
||||||
|
|
||||||
|
def on_scan_progress(self, msg):
|
||||||
|
self.log_output.append(msg)
|
||||||
|
sb = self.log_output.verticalScrollBar()
|
||||||
|
sb.setValue(sb.maximum())
|
||||||
|
|
||||||
|
def on_scan_finished(self, result):
|
||||||
|
self.scan_btn.setEnabled(True)
|
||||||
|
self.scan_btn.setText("Scan Network")
|
||||||
|
self.scan_btn.setStyleSheet("")
|
||||||
|
self.progress.hide()
|
||||||
|
|
||||||
|
scan_data = result.get('scan', {})
|
||||||
|
self.table.setRowCount(len(scan_data))
|
||||||
|
|
||||||
|
for i, (ip, data) in enumerate(scan_data.items()):
|
||||||
|
mac = "Unknown"
|
||||||
|
vendor = "Unknown"
|
||||||
|
status = data['status']['state']
|
||||||
|
|
||||||
|
if 'addresses' in data and 'mac' in data['addresses']:
|
||||||
|
mac = data['addresses']['mac']
|
||||||
|
if 'vendor' in data and mac in data['vendor']:
|
||||||
|
vendor = data['vendor'][mac]
|
||||||
|
|
||||||
|
# Collect Open Ports
|
||||||
|
open_ports = []
|
||||||
|
if 'tcp' in data:
|
||||||
|
for port, info in data['tcp'].items():
|
||||||
|
if info['state'] == 'open':
|
||||||
|
open_ports.append(f"{port}/tcp")
|
||||||
|
if 'udp' in data:
|
||||||
|
for port, info in data['udp'].items():
|
||||||
|
if info['state'] == 'open':
|
||||||
|
open_ports.append(f"{port}/udp")
|
||||||
|
|
||||||
|
ports_str = ", ".join(open_ports) if open_ports else ""
|
||||||
|
|
||||||
|
self.table.setItem(i, 0, QTableWidgetItem(ip))
|
||||||
|
self.table.setItem(i, 1, QTableWidgetItem(mac))
|
||||||
|
self.table.setItem(i, 2, QTableWidgetItem(vendor))
|
||||||
|
self.table.setItem(i, 3, QTableWidgetItem(status))
|
||||||
|
self.table.setItem(i, 4, QTableWidgetItem(ports_str))
|
||||||
|
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
|
def apply_filters(self):
|
||||||
|
filter_text = self.filter_combo.currentText().lower()
|
||||||
|
|
||||||
|
for row in range(self.table.rowCount()):
|
||||||
|
status_item = self.table.item(row, 3)
|
||||||
|
if not status_item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
status = status_item.text().lower()
|
||||||
|
|
||||||
|
if filter_text == "all":
|
||||||
|
self.table.setRowHidden(row, False)
|
||||||
|
elif filter_text == "up" and status == "up":
|
||||||
|
self.table.setRowHidden(row, False)
|
||||||
|
elif filter_text == "down" and status == "down":
|
||||||
|
self.table.setRowHidden(row, False)
|
||||||
|
else:
|
||||||
|
self.table.setRowHidden(row, True)
|
||||||
|
|
||||||
|
def on_scan_error(self, error):
|
||||||
|
self.scan_btn.setEnabled(True)
|
||||||
|
self.scan_btn.setText("Scan Network")
|
||||||
|
self.scan_btn.setStyleSheet("")
|
||||||
|
self.progress.hide()
|
||||||
|
|
||||||
|
if "stopped by user" in str(error):
|
||||||
|
return
|
||||||
|
|
||||||
|
QMessageBox.critical(self, "Scan Error", error)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
if self.scan_thread and self.scan_thread.isRunning():
|
||||||
|
self.scan_thread.stop()
|
||||||
|
event.accept()
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QComboBox, QPushButton, QTableWidget, QTableWidgetItem,
|
||||||
|
QHeaderView, QLineEdit, QMessageBox, QCheckBox, QSplitter, QGroupBox)
|
||||||
|
from PyQt6.QtGui import QColor
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from src.core.sniffer import NetworkSniffer
|
||||||
|
|
||||||
|
class SnifferWidget(QWidget):
|
||||||
|
def __init__(self, network_manager):
|
||||||
|
super().__init__()
|
||||||
|
self.nm = network_manager
|
||||||
|
self.sniffer = NetworkSniffer()
|
||||||
|
self.is_sniffing = False
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Controls
|
||||||
|
controls_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
self.iface_combo = QComboBox()
|
||||||
|
self.refresh_interfaces()
|
||||||
|
|
||||||
|
self.filter_input = QLineEdit()
|
||||||
|
self.filter_input.setPlaceholderText("BPF Filter (e.g. tcp port 80)")
|
||||||
|
|
||||||
|
self.ip_filter_input = QLineEdit()
|
||||||
|
self.ip_filter_input.setPlaceholderText("Filter IP (Src/Dst)")
|
||||||
|
self.ip_filter_input.textChanged.connect(self.apply_ip_filter)
|
||||||
|
|
||||||
|
self.start_btn = QPushButton("Start Sniffing")
|
||||||
|
self.start_btn.clicked.connect(self.toggle_sniffing)
|
||||||
|
|
||||||
|
controls_layout.addWidget(QLabel("Interface:"))
|
||||||
|
controls_layout.addWidget(self.iface_combo)
|
||||||
|
controls_layout.addWidget(QLabel("BPF:"))
|
||||||
|
controls_layout.addWidget(self.filter_input)
|
||||||
|
controls_layout.addWidget(QLabel("IP Filter:"))
|
||||||
|
controls_layout.addWidget(self.ip_filter_input)
|
||||||
|
controls_layout.addWidget(self.start_btn)
|
||||||
|
|
||||||
|
self.layout.addLayout(controls_layout)
|
||||||
|
|
||||||
|
# Splitter for Dual Consoles
|
||||||
|
self.splitter = QSplitter(Qt.Orientation.Vertical)
|
||||||
|
self.layout.addWidget(self.splitter)
|
||||||
|
|
||||||
|
# Top Console: All Traffic
|
||||||
|
self.all_traffic_group = QGroupBox("All Traffic")
|
||||||
|
self.all_traffic_layout = QVBoxLayout(self.all_traffic_group)
|
||||||
|
self.table_all = self.create_table()
|
||||||
|
self.all_traffic_layout.addWidget(self.table_all)
|
||||||
|
self.splitter.addWidget(self.all_traffic_group)
|
||||||
|
|
||||||
|
# Bottom Console: Failed / Slow / Anomalies
|
||||||
|
self.failed_traffic_group = QGroupBox("Failed / Slow / Anomalies")
|
||||||
|
self.failed_traffic_layout = QVBoxLayout(self.failed_traffic_group)
|
||||||
|
self.table_failed = self.create_table()
|
||||||
|
self.failed_traffic_layout.addWidget(self.table_failed)
|
||||||
|
self.splitter.addWidget(self.failed_traffic_group)
|
||||||
|
|
||||||
|
# Set initial sizes
|
||||||
|
self.splitter.setSizes([400, 200])
|
||||||
|
|
||||||
|
def create_table(self):
|
||||||
|
table = QTableWidget()
|
||||||
|
table.setColumnCount(6)
|
||||||
|
table.setHorizontalHeaderLabels(["Time", "Source", "Destination", "Protocol", "Info", "Status"])
|
||||||
|
table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||||
|
table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
table.setAlternatingRowColors(True)
|
||||||
|
return table
|
||||||
|
|
||||||
|
def refresh_interfaces(self):
|
||||||
|
self.iface_combo.clear()
|
||||||
|
interfaces = self.nm.list_interfaces()
|
||||||
|
for iface in interfaces:
|
||||||
|
self.iface_combo.addItem(iface['name'])
|
||||||
|
|
||||||
|
def toggle_sniffing(self):
|
||||||
|
if self.is_sniffing:
|
||||||
|
self.sniffer.stop_sniffing()
|
||||||
|
self.start_btn.setText("Start Sniffing")
|
||||||
|
self.start_btn.setStyleSheet("background-color: #007acc;")
|
||||||
|
self.is_sniffing = False
|
||||||
|
else:
|
||||||
|
iface = self.iface_combo.currentText()
|
||||||
|
filter_str = self.filter_input.text()
|
||||||
|
|
||||||
|
self.table_all.setRowCount(0)
|
||||||
|
self.table_failed.setRowCount(0)
|
||||||
|
|
||||||
|
self.sniffer_thread = self.sniffer.start_sniffing(iface, filter_str)
|
||||||
|
self.sniffer_thread.packet_received.connect(self.add_packet)
|
||||||
|
self.sniffer_thread.error_occurred.connect(self.on_error)
|
||||||
|
|
||||||
|
self.start_btn.setText("Stop Sniffing")
|
||||||
|
self.start_btn.setStyleSheet("background-color: #d32f2f;")
|
||||||
|
self.is_sniffing = True
|
||||||
|
|
||||||
|
def add_packet(self, pkt):
|
||||||
|
# Add to All Traffic
|
||||||
|
self.insert_packet_row(self.table_all, pkt)
|
||||||
|
|
||||||
|
# Check if failed/slow/anomaly
|
||||||
|
status = pkt.get('status', '')
|
||||||
|
if status in ['Error', 'Connecting']:
|
||||||
|
self.insert_packet_row(self.table_failed, pkt)
|
||||||
|
|
||||||
|
def insert_packet_row(self, table, pkt):
|
||||||
|
row = table.rowCount()
|
||||||
|
table.insertRow(row)
|
||||||
|
table.setItem(row, 0, QTableWidgetItem(pkt['time']))
|
||||||
|
table.setItem(row, 1, QTableWidgetItem(pkt['src']))
|
||||||
|
table.setItem(row, 2, QTableWidgetItem(pkt['dst']))
|
||||||
|
table.setItem(row, 3, QTableWidgetItem(str(pkt['proto'])))
|
||||||
|
table.setItem(row, 4, QTableWidgetItem(pkt['info']))
|
||||||
|
|
||||||
|
status_item = QTableWidgetItem(pkt.get('status', ''))
|
||||||
|
if pkt.get('status') == 'Error':
|
||||||
|
status_item.setForeground(QColor('#ff5252')) # Red
|
||||||
|
elif pkt.get('status') == 'Connecting':
|
||||||
|
status_item.setForeground(QColor('#ffab40')) # Orange
|
||||||
|
elif pkt.get('status') == 'OK':
|
||||||
|
status_item.setForeground(QColor('#69f0ae')) # Green
|
||||||
|
|
||||||
|
table.setItem(row, 5, status_item)
|
||||||
|
|
||||||
|
# Check filter immediately
|
||||||
|
self.check_filter_for_row(table, row, pkt['src'], pkt['dst'])
|
||||||
|
|
||||||
|
if not table.isRowHidden(row):
|
||||||
|
table.scrollToBottom()
|
||||||
|
|
||||||
|
def apply_ip_filter(self):
|
||||||
|
filter_ip = self.ip_filter_input.text().strip()
|
||||||
|
|
||||||
|
for table in [self.table_all, self.table_failed]:
|
||||||
|
for row in range(table.rowCount()):
|
||||||
|
src = table.item(row, 1).text()
|
||||||
|
dst = table.item(row, 2).text()
|
||||||
|
|
||||||
|
if not filter_ip:
|
||||||
|
table.setRowHidden(row, False)
|
||||||
|
elif filter_ip in src or filter_ip in dst:
|
||||||
|
table.setRowHidden(row, False)
|
||||||
|
else:
|
||||||
|
table.setRowHidden(row, True)
|
||||||
|
|
||||||
|
def check_filter_for_row(self, table, row, src, dst):
|
||||||
|
filter_ip = self.ip_filter_input.text().strip()
|
||||||
|
if not filter_ip:
|
||||||
|
table.setRowHidden(row, False)
|
||||||
|
return
|
||||||
|
|
||||||
|
if filter_ip in src or filter_ip in dst:
|
||||||
|
table.setRowHidden(row, False)
|
||||||
|
else:
|
||||||
|
table.setRowHidden(row, True)
|
||||||
|
|
||||||
|
def on_error(self, msg):
|
||||||
|
self.toggle_sniffing()
|
||||||
|
QMessageBox.critical(self, "Sniffer Error", msg)
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QComboBox, QSpinBox, QPushButton, QGroupBox,
|
||||||
|
QMessageBox, QFormLayout, QLineEdit, QTableWidget,
|
||||||
|
QTableWidgetItem, QHeaderView, QCheckBox, QSplitter)
|
||||||
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
|
from src.core.vlan_scanner import VLANScanner
|
||||||
|
|
||||||
|
class VLANBridgeWidget(QWidget):
|
||||||
|
def __init__(self, network_manager):
|
||||||
|
super().__init__()
|
||||||
|
self.nm = network_manager
|
||||||
|
self.vlan_scanner = VLANScanner()
|
||||||
|
self.scan_thread = None
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Create a horizontal layout for the top part (Bridge + Wifi)
|
||||||
|
self.splitter = QSplitter(Qt.Orientation.Vertical)
|
||||||
|
self.layout.addWidget(self.splitter)
|
||||||
|
|
||||||
|
# Top Container Widget
|
||||||
|
top_widget = QWidget()
|
||||||
|
top_layout = QHBoxLayout(top_widget)
|
||||||
|
top_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# Left Column: Wired Bridging
|
||||||
|
left_col = QVBoxLayout()
|
||||||
|
|
||||||
|
# Wired Bridging Group
|
||||||
|
wired_group = QGroupBox("Wired Bridging")
|
||||||
|
wired_layout = QVBoxLayout(wired_group)
|
||||||
|
wired_layout.setSpacing(5)
|
||||||
|
wired_layout.setContentsMargins(10, 15, 10, 10)
|
||||||
|
|
||||||
|
# Mode Selection
|
||||||
|
mode_layout = QHBoxLayout()
|
||||||
|
mode_layout.addWidget(QLabel("Mode:"))
|
||||||
|
self.mode_combo = QComboBox()
|
||||||
|
self.mode_combo.addItems(["VLAN Trunk (Trunk -> Access)", "Passthrough (Untagged -> Untagged)"])
|
||||||
|
self.mode_combo.currentIndexChanged.connect(self.toggle_mode_inputs)
|
||||||
|
mode_layout.addWidget(self.mode_combo)
|
||||||
|
wired_layout.addLayout(mode_layout)
|
||||||
|
|
||||||
|
# Interface Selection
|
||||||
|
form_layout = QFormLayout()
|
||||||
|
form_layout.setSpacing(5)
|
||||||
|
self.trunk_combo = QComboBox() # Input
|
||||||
|
self.trunk_combo.currentIndexChanged.connect(self.on_trunk_combo_changed)
|
||||||
|
self.access_combo = QComboBox() # Output
|
||||||
|
|
||||||
|
self.lbl_input = QLabel("Input Interface (Trunk):")
|
||||||
|
self.lbl_output = QLabel("Output Interface (Access):")
|
||||||
|
|
||||||
|
form_layout.addRow(self.lbl_input, self.trunk_combo)
|
||||||
|
form_layout.addRow(self.lbl_output, self.access_combo)
|
||||||
|
|
||||||
|
# VLAN ID Input
|
||||||
|
self.vlan_input = QSpinBox()
|
||||||
|
self.vlan_input.setRange(1, 4094)
|
||||||
|
self.lbl_vlan = QLabel("VLAN ID:")
|
||||||
|
form_layout.addRow(self.lbl_vlan, self.vlan_input)
|
||||||
|
|
||||||
|
wired_layout.addLayout(form_layout)
|
||||||
|
|
||||||
|
# IP / DHCP Configuration
|
||||||
|
ip_group = QGroupBox("Bridge IP & DHCP Server")
|
||||||
|
ip_layout = QFormLayout(ip_group)
|
||||||
|
ip_layout.setSpacing(5)
|
||||||
|
|
||||||
|
self.enable_dhcp_cb = QCheckBox("Enable DHCP Server & Static IP")
|
||||||
|
self.enable_dhcp_cb.toggled.connect(self.toggle_dhcp_inputs)
|
||||||
|
ip_layout.addRow(self.enable_dhcp_cb)
|
||||||
|
|
||||||
|
self.bridge_ip = QLineEdit("192.168.50.1")
|
||||||
|
self.dhcp_start = QLineEdit("192.168.50.10")
|
||||||
|
self.dhcp_end = QLineEdit("192.168.50.100")
|
||||||
|
|
||||||
|
ip_layout.addRow("Bridge IP (Gateway):", self.bridge_ip)
|
||||||
|
ip_layout.addRow("DHCP Start:", self.dhcp_start)
|
||||||
|
ip_layout.addRow("DHCP End:", self.dhcp_end)
|
||||||
|
|
||||||
|
wired_layout.addWidget(ip_group)
|
||||||
|
self.ip_group = ip_group
|
||||||
|
|
||||||
|
# Initialize inputs state
|
||||||
|
self.toggle_dhcp_inputs(False)
|
||||||
|
|
||||||
|
self.bridge_btn = QPushButton("Create Bridge")
|
||||||
|
self.bridge_btn.clicked.connect(self.create_bridge)
|
||||||
|
wired_layout.addWidget(self.bridge_btn)
|
||||||
|
|
||||||
|
# Bridge Management Group
|
||||||
|
manage_group = QGroupBox("Manage Bridges")
|
||||||
|
manage_layout = QHBoxLayout(manage_group)
|
||||||
|
manage_layout.setContentsMargins(5, 15, 5, 5)
|
||||||
|
|
||||||
|
self.active_bridges_combo = QComboBox()
|
||||||
|
self.delete_bridge_btn = QPushButton("Delete Bridge")
|
||||||
|
self.delete_bridge_btn.setStyleSheet("background-color: #800000; color: white;")
|
||||||
|
self.delete_bridge_btn.clicked.connect(self.delete_bridge)
|
||||||
|
|
||||||
|
manage_layout.addWidget(QLabel("Active Bridges:"))
|
||||||
|
manage_layout.addWidget(self.active_bridges_combo)
|
||||||
|
manage_layout.addWidget(self.delete_bridge_btn)
|
||||||
|
|
||||||
|
wired_layout.addWidget(manage_group)
|
||||||
|
|
||||||
|
left_col.addWidget(wired_group)
|
||||||
|
top_layout.addLayout(left_col)
|
||||||
|
|
||||||
|
# Right Column: Wi-Fi Bridging
|
||||||
|
right_col = QVBoxLayout()
|
||||||
|
wifi_group = QGroupBox("Wi-Fi AP Bridging")
|
||||||
|
wifi_layout = QVBoxLayout(wifi_group)
|
||||||
|
wifi_layout.setSpacing(5)
|
||||||
|
wifi_layout.setContentsMargins(10, 15, 10, 10)
|
||||||
|
|
||||||
|
wifi_form = QFormLayout()
|
||||||
|
wifi_form.setSpacing(5)
|
||||||
|
self.wifi_iface_combo = QComboBox()
|
||||||
|
self.wifi_iface_combo.currentIndexChanged.connect(self.on_wifi_iface_changed)
|
||||||
|
self.ssid_input = QLineEdit()
|
||||||
|
self.password_input = QLineEdit()
|
||||||
|
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
self.bridge_select_combo = QComboBox() # Select which bridge to attach to
|
||||||
|
|
||||||
|
wifi_form.addRow("Wi-Fi Interface:", self.wifi_iface_combo)
|
||||||
|
wifi_form.addRow("SSID:", self.ssid_input)
|
||||||
|
wifi_form.addRow("Password:", self.password_input)
|
||||||
|
wifi_form.addRow("Attach to Bridge:", self.bridge_select_combo)
|
||||||
|
|
||||||
|
wifi_layout.addLayout(wifi_form)
|
||||||
|
|
||||||
|
# Wi-Fi DHCP Configuration
|
||||||
|
wifi_dhcp_group = QGroupBox("Wi-Fi DHCP Server")
|
||||||
|
wifi_dhcp_layout = QFormLayout(wifi_dhcp_group)
|
||||||
|
wifi_dhcp_layout.setSpacing(5)
|
||||||
|
|
||||||
|
self.wifi_enable_dhcp_cb = QCheckBox("Enable DHCP on Bridge")
|
||||||
|
self.wifi_enable_dhcp_cb.toggled.connect(self.toggle_wifi_dhcp_inputs)
|
||||||
|
wifi_dhcp_layout.addRow(self.wifi_enable_dhcp_cb)
|
||||||
|
|
||||||
|
self.wifi_bridge_ip = QLineEdit("192.168.60.1")
|
||||||
|
self.wifi_dhcp_start = QLineEdit("192.168.60.10")
|
||||||
|
self.wifi_dhcp_end = QLineEdit("192.168.60.100")
|
||||||
|
|
||||||
|
wifi_dhcp_layout.addRow("Bridge IP:", self.wifi_bridge_ip)
|
||||||
|
wifi_dhcp_layout.addRow("DHCP Start:", self.wifi_dhcp_start)
|
||||||
|
wifi_dhcp_layout.addRow("DHCP End:", self.wifi_dhcp_end)
|
||||||
|
|
||||||
|
wifi_layout.addWidget(wifi_dhcp_group)
|
||||||
|
self.wifi_dhcp_group = wifi_dhcp_group
|
||||||
|
|
||||||
|
# Initialize Wi-Fi DHCP inputs state
|
||||||
|
self.toggle_wifi_dhcp_inputs(False)
|
||||||
|
|
||||||
|
wifi_btns_layout = QHBoxLayout()
|
||||||
|
self.wifi_btn = QPushButton("Start Wi-Fi AP")
|
||||||
|
self.wifi_btn.clicked.connect(self.start_wifi)
|
||||||
|
wifi_btns_layout.addWidget(self.wifi_btn)
|
||||||
|
|
||||||
|
self.stop_wifi_btn = QPushButton("Stop Wi-Fi AP")
|
||||||
|
self.stop_wifi_btn.clicked.connect(self.stop_wifi)
|
||||||
|
self.stop_wifi_btn.setEnabled(False)
|
||||||
|
wifi_btns_layout.addWidget(self.stop_wifi_btn)
|
||||||
|
|
||||||
|
wifi_layout.addLayout(wifi_btns_layout)
|
||||||
|
|
||||||
|
# Connected Clients
|
||||||
|
self.clients_group = QGroupBox("Connected Clients")
|
||||||
|
clients_layout = QVBoxLayout(self.clients_group)
|
||||||
|
clients_layout.setContentsMargins(5, 15, 5, 5)
|
||||||
|
self.clients_table = QTableWidget()
|
||||||
|
self.clients_table.setColumnCount(3)
|
||||||
|
self.clients_table.setHorizontalHeaderLabels(["MAC", "IP", "Hostname"])
|
||||||
|
self.clients_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.clients_table.setMaximumHeight(150)
|
||||||
|
clients_layout.addWidget(self.clients_table)
|
||||||
|
wifi_layout.addWidget(self.clients_group)
|
||||||
|
|
||||||
|
right_col.addWidget(wifi_group)
|
||||||
|
top_layout.addLayout(right_col)
|
||||||
|
|
||||||
|
# Client Refresh Timer
|
||||||
|
self.client_timer = QTimer()
|
||||||
|
self.client_timer.timeout.connect(self.refresh_clients)
|
||||||
|
self.client_timer.setInterval(5000) # 5 seconds
|
||||||
|
|
||||||
|
self.splitter.addWidget(top_widget)
|
||||||
|
|
||||||
|
# Bottom Part: VLAN Scanner
|
||||||
|
scanner_group = QGroupBox("Passive VLAN Scanner")
|
||||||
|
scanner_layout = QVBoxLayout(scanner_group)
|
||||||
|
scanner_layout.setContentsMargins(10, 15, 10, 10)
|
||||||
|
|
||||||
|
scan_controls = QHBoxLayout()
|
||||||
|
self.scan_iface_combo = QComboBox()
|
||||||
|
self.scan_btn = QPushButton("Start Scan (10s)")
|
||||||
|
self.scan_btn.clicked.connect(self.start_scan)
|
||||||
|
|
||||||
|
scan_controls.addWidget(QLabel("Interface:"))
|
||||||
|
scan_controls.addWidget(self.scan_iface_combo)
|
||||||
|
scan_controls.addWidget(self.scan_btn)
|
||||||
|
|
||||||
|
scanner_layout.addLayout(scan_controls)
|
||||||
|
|
||||||
|
self.scan_table = QTableWidget()
|
||||||
|
self.scan_table.setColumnCount(2)
|
||||||
|
self.scan_table.setHorizontalHeaderLabels(["VLAN ID", "Packets Detected"])
|
||||||
|
self.scan_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.scan_table.setMinimumHeight(200) # Ensure it has space
|
||||||
|
scanner_layout.addWidget(self.scan_table)
|
||||||
|
|
||||||
|
self.port_type_label = QLabel("Port Type: Unknown")
|
||||||
|
self.port_type_label.setStyleSheet("font-weight: bold; color: #aaaaaa;")
|
||||||
|
scanner_layout.addWidget(self.port_type_label)
|
||||||
|
|
||||||
|
self.splitter.addWidget(scanner_group)
|
||||||
|
|
||||||
|
# Set initial sizes for splitter (Top, Bottom)
|
||||||
|
self.splitter.setSizes([400, 300])
|
||||||
|
|
||||||
|
self.refresh_interfaces()
|
||||||
|
self.toggle_mode_inputs() # Initialize UI based on default mode
|
||||||
|
|
||||||
|
def refresh_interfaces(self):
|
||||||
|
interfaces = self.nm.list_interfaces()
|
||||||
|
self.trunk_combo.clear()
|
||||||
|
self.access_combo.clear()
|
||||||
|
self.scan_iface_combo.clear()
|
||||||
|
self.wifi_iface_combo.clear()
|
||||||
|
|
||||||
|
for iface in interfaces:
|
||||||
|
name = iface['name']
|
||||||
|
self.trunk_combo.addItem(name)
|
||||||
|
self.access_combo.addItem(name)
|
||||||
|
self.scan_iface_combo.addItem(name)
|
||||||
|
if iface['type'] == 'Wireless':
|
||||||
|
self.wifi_iface_combo.addItem(name)
|
||||||
|
|
||||||
|
# Populate bridges
|
||||||
|
self.bridge_select_combo.clear()
|
||||||
|
self.active_bridges_combo.clear()
|
||||||
|
|
||||||
|
bridges = [iface['name'] for iface in interfaces if iface['name'].startswith('br')]
|
||||||
|
|
||||||
|
for br in bridges:
|
||||||
|
self.bridge_select_combo.addItem(br)
|
||||||
|
self.active_bridges_combo.addItem(br)
|
||||||
|
|
||||||
|
# Add manual entry option for Wi-Fi bridge selection if needed
|
||||||
|
self.bridge_select_combo.setEditable(True)
|
||||||
|
|
||||||
|
def toggle_mode_inputs(self):
|
||||||
|
mode = self.mode_combo.currentText()
|
||||||
|
if "Passthrough" in mode:
|
||||||
|
self.lbl_input.setText("Interface 1:")
|
||||||
|
self.lbl_output.setText("Interface 2:")
|
||||||
|
self.lbl_vlan.setVisible(False)
|
||||||
|
self.vlan_input.setVisible(False)
|
||||||
|
else:
|
||||||
|
self.lbl_input.setText("Input Interface (Trunk):")
|
||||||
|
self.lbl_output.setText("Output Interface (Access):")
|
||||||
|
self.lbl_vlan.setVisible(True)
|
||||||
|
self.lbl_vlan.setVisible(True)
|
||||||
|
self.vlan_input.setVisible(True)
|
||||||
|
|
||||||
|
def toggle_dhcp_inputs(self, checked):
|
||||||
|
self.bridge_ip.setEnabled(checked)
|
||||||
|
self.dhcp_start.setEnabled(checked)
|
||||||
|
self.dhcp_end.setEnabled(checked)
|
||||||
|
|
||||||
|
def toggle_wifi_dhcp_inputs(self, checked):
|
||||||
|
self.wifi_bridge_ip.setEnabled(checked)
|
||||||
|
self.wifi_dhcp_start.setEnabled(checked)
|
||||||
|
self.wifi_dhcp_end.setEnabled(checked)
|
||||||
|
|
||||||
|
def on_trunk_combo_changed(self, index):
|
||||||
|
iface_name = self.trunk_combo.currentText()
|
||||||
|
self.autofill_dhcp(iface_name, self.bridge_ip, self.dhcp_start, self.dhcp_end)
|
||||||
|
|
||||||
|
def on_wifi_iface_changed(self, index):
|
||||||
|
iface_name = self.wifi_iface_combo.currentText()
|
||||||
|
self.autofill_dhcp(iface_name, self.wifi_bridge_ip, self.wifi_dhcp_start, self.wifi_dhcp_end)
|
||||||
|
|
||||||
|
def autofill_dhcp(self, iface_name, ip_field, start_field, end_field):
|
||||||
|
if not iface_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get config to auto-fill DHCP
|
||||||
|
config = self.nm.get_interface_config(iface_name)
|
||||||
|
ip = config.get('ip', '')
|
||||||
|
|
||||||
|
if ip:
|
||||||
|
# Suggest a subnet based on this IP
|
||||||
|
parts = ip.split('.')
|
||||||
|
if len(parts) == 4:
|
||||||
|
base = ".".join(parts[:3])
|
||||||
|
ip_field.setText(ip)
|
||||||
|
start_field.setText(f"{base}.100")
|
||||||
|
end_field.setText(f"{base}.200")
|
||||||
|
|
||||||
|
def create_bridge(self):
|
||||||
|
mode = self.mode_combo.currentText()
|
||||||
|
iface1 = self.trunk_combo.currentText()
|
||||||
|
iface2 = self.access_combo.currentText()
|
||||||
|
|
||||||
|
if iface1 == iface2:
|
||||||
|
QMessageBox.warning(self, "Error", "Interfaces must be different")
|
||||||
|
return
|
||||||
|
|
||||||
|
success = False
|
||||||
|
msg = ""
|
||||||
|
bridge_name = ""
|
||||||
|
|
||||||
|
if "Passthrough" in mode:
|
||||||
|
success, msg = self.nm.create_simple_bridge(iface1, iface2)
|
||||||
|
bridge_name = "br_pass"
|
||||||
|
else:
|
||||||
|
vlan_id = self.vlan_input.value()
|
||||||
|
success, msg = self.nm.create_vlan_bridge(iface1, vlan_id, iface2)
|
||||||
|
bridge_name = f"br{vlan_id}"
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Handle IP/DHCP
|
||||||
|
if self.enable_dhcp_cb.isChecked():
|
||||||
|
ip = self.bridge_ip.text()
|
||||||
|
start = self.dhcp_start.text()
|
||||||
|
end = self.dhcp_end.text()
|
||||||
|
|
||||||
|
# Start DHCP
|
||||||
|
d_success, d_msg = self.nm.start_dhcp_server(bridge_name, ip, start, end)
|
||||||
|
if d_success:
|
||||||
|
msg += f"\nDHCP Server running on {ip}"
|
||||||
|
else:
|
||||||
|
msg += f"\nDHCP Error: {d_msg}"
|
||||||
|
|
||||||
|
QMessageBox.information(self, "Success", msg)
|
||||||
|
self.bridge_select_combo.addItem(bridge_name)
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Error", msg)
|
||||||
|
|
||||||
|
def start_wifi(self):
|
||||||
|
wifi_iface = self.wifi_iface_combo.currentText()
|
||||||
|
ssid = self.ssid_input.text()
|
||||||
|
password = self.password_input.text()
|
||||||
|
bridge = self.bridge_select_combo.currentText()
|
||||||
|
|
||||||
|
if not wifi_iface or not ssid or not password or not bridge:
|
||||||
|
QMessageBox.warning(self, "Error", "Please fill all Wi-Fi fields.")
|
||||||
|
return
|
||||||
|
|
||||||
|
success, msg = self.nm.setup_wifi_ap(wifi_iface, ssid, password, bridge)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Handle DHCP for Wi-Fi
|
||||||
|
if self.wifi_enable_dhcp_cb.isChecked():
|
||||||
|
ip = self.wifi_bridge_ip.text()
|
||||||
|
start = self.wifi_dhcp_start.text()
|
||||||
|
end = self.wifi_dhcp_end.text()
|
||||||
|
|
||||||
|
# Start DHCP on the bridge
|
||||||
|
d_success, d_msg = self.nm.start_dhcp_server(bridge, ip, start, end)
|
||||||
|
if d_success:
|
||||||
|
msg += f"\nDHCP Server running on {ip}"
|
||||||
|
else:
|
||||||
|
msg += f"\nDHCP Error: {d_msg}"
|
||||||
|
|
||||||
|
self.wifi_btn.setEnabled(False)
|
||||||
|
self.stop_wifi_btn.setEnabled(True)
|
||||||
|
self.client_timer.start()
|
||||||
|
QMessageBox.information(self, "Success", msg)
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Error", f"Failed to start Wi-Fi AP: {msg}")
|
||||||
|
|
||||||
|
def delete_bridge(self):
|
||||||
|
bridge = self.active_bridges_combo.currentText()
|
||||||
|
if not bridge:
|
||||||
|
return
|
||||||
|
|
||||||
|
confirm = QMessageBox.question(self, "Confirm Deletion",
|
||||||
|
f"Are you sure you want to delete bridge '{bridge}'?\nThis will stop any associated DHCP servers.",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
|
||||||
|
if confirm == QMessageBox.StandardButton.Yes:
|
||||||
|
success, msg = self.nm.delete_bridge(bridge)
|
||||||
|
if success:
|
||||||
|
QMessageBox.information(self, "Success", msg)
|
||||||
|
self.refresh_interfaces()
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Error", msg)
|
||||||
|
|
||||||
|
def stop_wifi(self):
|
||||||
|
iface = self.wifi_iface_combo.currentText()
|
||||||
|
success, msg = self.nm.stop_wifi_ap(iface)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.wifi_btn.setEnabled(True)
|
||||||
|
self.stop_wifi_btn.setEnabled(False)
|
||||||
|
self.client_timer.stop()
|
||||||
|
self.clients_table.setRowCount(0)
|
||||||
|
QMessageBox.information(self, "Success", msg)
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Error", msg)
|
||||||
|
|
||||||
|
def refresh_clients(self):
|
||||||
|
iface = self.wifi_iface_combo.currentText()
|
||||||
|
clients = self.nm.get_wifi_clients(iface)
|
||||||
|
|
||||||
|
self.clients_table.setRowCount(len(clients))
|
||||||
|
for i, client in enumerate(clients):
|
||||||
|
self.clients_table.setItem(i, 0, QTableWidgetItem(client['mac']))
|
||||||
|
self.clients_table.setItem(i, 1, QTableWidgetItem(client['ip']))
|
||||||
|
self.clients_table.setItem(i, 2, QTableWidgetItem(client['hostname']))
|
||||||
|
|
||||||
|
def start_scan(self):
|
||||||
|
if self.scan_btn.text() == "Stop Scan":
|
||||||
|
if self.scan_thread:
|
||||||
|
self.scan_thread.stop()
|
||||||
|
self.scan_btn.setEnabled(False)
|
||||||
|
self.scan_btn.setText("Stopping...")
|
||||||
|
return
|
||||||
|
|
||||||
|
iface = self.scan_iface_combo.currentText()
|
||||||
|
self.scan_table.setRowCount(0)
|
||||||
|
self.port_type_label.setText("Port Type: Scanning...")
|
||||||
|
self.port_type_label.setStyleSheet("font-weight: bold; color: #e0e000;")
|
||||||
|
|
||||||
|
self.scan_btn.setText("Stop Scan")
|
||||||
|
self.scan_btn.setStyleSheet("background-color: #800000; color: white;")
|
||||||
|
|
||||||
|
self.scan_thread = self.vlan_scanner.scan(iface)
|
||||||
|
self.scan_thread.vlan_detected.connect(self.on_vlan_detected)
|
||||||
|
self.scan_thread.untagged_detected.connect(self.on_untagged_detected)
|
||||||
|
self.scan_thread.scan_finished.connect(self.on_scan_finished)
|
||||||
|
self.scan_thread.error_occurred.connect(self.on_scan_error)
|
||||||
|
|
||||||
|
def on_untagged_detected(self, count):
|
||||||
|
self.update_table_row("Untagged (Native)", count)
|
||||||
|
self.update_port_analysis()
|
||||||
|
|
||||||
|
def on_vlan_detected(self, vlan_id, count):
|
||||||
|
self.update_table_row(str(vlan_id), count)
|
||||||
|
self.update_port_analysis()
|
||||||
|
|
||||||
|
def update_table_row(self, label, count):
|
||||||
|
# Check if row exists
|
||||||
|
found = False
|
||||||
|
for row in range(self.scan_table.rowCount()):
|
||||||
|
if self.scan_table.item(row, 0).text() == label:
|
||||||
|
self.scan_table.setItem(row, 1, QTableWidgetItem(str(count)))
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
row = self.scan_table.rowCount()
|
||||||
|
self.scan_table.insertRow(row)
|
||||||
|
self.scan_table.setItem(row, 0, QTableWidgetItem(label))
|
||||||
|
self.scan_table.setItem(row, 1, QTableWidgetItem(str(count)))
|
||||||
|
|
||||||
|
def update_port_analysis(self):
|
||||||
|
# Analyze table content to guess port type
|
||||||
|
has_untagged = False
|
||||||
|
vlan_count = 0
|
||||||
|
|
||||||
|
for row in range(self.scan_table.rowCount()):
|
||||||
|
label = self.scan_table.item(row, 0).text()
|
||||||
|
if label == "Untagged (Native)":
|
||||||
|
has_untagged = True
|
||||||
|
else:
|
||||||
|
vlan_count += 1
|
||||||
|
|
||||||
|
if vlan_count > 1:
|
||||||
|
msg = f"Trunk Port (Detected {vlan_count} VLANs)"
|
||||||
|
color = "#00e000" # Green
|
||||||
|
elif vlan_count == 1 and has_untagged:
|
||||||
|
msg = "Hybrid Port (Tagged + Untagged)"
|
||||||
|
color = "#e0e000" # Yellow
|
||||||
|
elif vlan_count == 1:
|
||||||
|
msg = "Trunk Port (Single VLAN)"
|
||||||
|
color = "#00e000"
|
||||||
|
elif has_untagged:
|
||||||
|
msg = "Access Port (Untagged Traffic Only)"
|
||||||
|
color = "#00e0e0" # Cyan
|
||||||
|
else:
|
||||||
|
msg = "Unknown / No Traffic"
|
||||||
|
color = "#aaaaaa"
|
||||||
|
|
||||||
|
self.port_type_label.setText(f"Port Type: {msg}")
|
||||||
|
self.port_type_label.setStyleSheet(f"font-weight: bold; color: {color};")
|
||||||
|
|
||||||
|
def on_scan_finished(self):
|
||||||
|
self.scan_btn.setEnabled(True)
|
||||||
|
self.scan_btn.setText("Start Scan (10s)")
|
||||||
|
self.scan_btn.setStyleSheet("")
|
||||||
|
# Don't show popup if just stopped, but hard to distinguish here without flag.
|
||||||
|
# Let's just show it, or maybe check if it was manual stop?
|
||||||
|
# For now, simple message is fine.
|
||||||
|
# QMessageBox.information(self, "Scan Finished", "VLAN Scan completed.")
|
||||||
|
|
||||||
|
def on_scan_error(self, msg):
|
||||||
|
self.scan_btn.setEnabled(True)
|
||||||
|
self.scan_btn.setText("Start Scan (10s)")
|
||||||
|
self.scan_btn.setStyleSheet("")
|
||||||
|
QMessageBox.critical(self, "Error", msg)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
if self.scan_thread and self.scan_thread.isRunning():
|
||||||
|
self.scan_thread.stop()
|
||||||
|
event.accept()
|
||||||
Binary file not shown.
@@ -0,0 +1,104 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
class ReportGenerator:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def generate_html_report(self, interfaces, scan_results, sniff_all, sniff_failed, filename="report.html"):
|
||||||
|
"""
|
||||||
|
Generates an HTML report with the provided data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
html = f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>JNC-NetTools Report</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: 'Segoe UI', sans-serif; background-color: #f4f4f4; color: #333; margin: 0; padding: 20px; }}
|
||||||
|
.container {{ max-width: 1000px; margin: 0 auto; background: white; padding: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); border-radius: 8px; }}
|
||||||
|
h1 {{ color: #0078d7; border-bottom: 3px solid #0078d7; padding-bottom: 10px; margin-bottom: 30px; }}
|
||||||
|
h2 {{ color: #444; margin-top: 40px; border-left: 5px solid #0078d7; padding-left: 10px; }}
|
||||||
|
table {{ width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.9em; }}
|
||||||
|
th, td {{ border: 1px solid #e0e0e0; padding: 10px; text-align: left; }}
|
||||||
|
th {{ background-color: #f8f9fa; color: #333; font-weight: 600; }}
|
||||||
|
tr:nth-child(even) {{ background-color: #fcfcfc; }}
|
||||||
|
.timestamp {{ color: #666; font-size: 0.9em; margin-bottom: 20px; }}
|
||||||
|
.status-up {{ color: green; font-weight: bold; }}
|
||||||
|
.status-down {{ color: red; font-weight: bold; }}
|
||||||
|
.error-row {{ background-color: #fff0f0 !important; }}
|
||||||
|
.error-text {{ color: #d32f2f; font-weight: bold; }}
|
||||||
|
.badge {{ padding: 3px 8px; border-radius: 12px; font-size: 0.85em; color: white; }}
|
||||||
|
.badge-tcp {{ background-color: #0078d7; }}
|
||||||
|
.badge-udp {{ background-color: #e65100; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>JNC-NetTools Diagnostic Report</h1>
|
||||||
|
<p class="timestamp">Generated on: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
|
||||||
|
|
||||||
|
<h2>Network Interfaces</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>Name</th><th>Type</th><th>Status</th><th>IP Address</th><th>MAC Address</th></tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
for iface in interfaces:
|
||||||
|
ip = iface['ipv4'][0] if iface['ipv4'] else "N/A"
|
||||||
|
status_class = "status-up" if iface['is_up'] else "status-down"
|
||||||
|
html += f"<tr><td><b>{iface['name']}</b></td><td>{iface['type']}</td><td class='{status_class}'>{iface['status']}</td><td>{ip}</td><td>{iface['mac']}</td></tr>"
|
||||||
|
|
||||||
|
html += """
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Port Scan Results</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>IP</th><th>MAC</th><th>Vendor</th><th>Status</th></tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not scan_results:
|
||||||
|
html += "<tr><td colspan='4' style='text-align:center; color:#888;'>No scan results available.</td></tr>"
|
||||||
|
else:
|
||||||
|
for item in scan_results:
|
||||||
|
html += f"<tr><td>{item['ip']}</td><td>{item['mac']}</td><td>{item['vendor']}</td><td>{item['status']}</td></tr>"
|
||||||
|
|
||||||
|
html += """
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Anomalies / Failed Traffic (Errors & Retransmissions)</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>Time</th><th>Source</th><th>Destination</th><th>Protocol</th><th>Info</th><th>Status</th></tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not sniff_failed:
|
||||||
|
html += "<tr><td colspan='6' style='text-align:center; color:#888;'>No anomalies detected.</td></tr>"
|
||||||
|
else:
|
||||||
|
for pkt in sniff_failed:
|
||||||
|
html += f"<tr class='error-row'><td>{pkt['time']}</td><td>{pkt['src']}</td><td>{pkt['dst']}</td><td>{pkt['proto']}</td><td>{pkt['info']}</td><td class='error-text'>{pkt['status']}</td></tr>"
|
||||||
|
|
||||||
|
html += """
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Traffic Log (Last 100 Packets)</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>Time</th><th>Source</th><th>Destination</th><th>Protocol</th><th>Info</th><th>Status</th></tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Limit to last 100 for readability
|
||||||
|
for pkt in sniff_all[-100:]:
|
||||||
|
html += f"<tr><td>{pkt['time']}</td><td>{pkt['src']}</td><td>{pkt['dst']}</td><td>{pkt['proto']}</td><td>{pkt['info']}</td><td>{pkt['status']}</td></tr>"
|
||||||
|
|
||||||
|
html += """
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
f.write(html)
|
||||||
|
return True, os.path.abspath(filename)
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# JNC-Scan Walkthrough
|
||||||
|
|
||||||
|
This guide explains how to run and verify the **JNC-Scan** network diagnostic tool.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Ensure you have the required system dependencies:
|
||||||
|
```bash
|
||||||
|
sudo apt install python3-pyqt6 nmap tcpdump bridge-utils hostapd dnsmasq python3-venv libxcb-cursor0 ethtool
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation (Virtual Environment)
|
||||||
|
|
||||||
|
Since modern Linux distributions manage Python packages externally, we should use a virtual environment:
|
||||||
|
|
||||||
|
1. **Create a virtual environment**:
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Activate it and install dependencies**:
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
**CRITICAL**: The application requires **root privileges**. When using a virtual environment, point `sudo` to the venv's Python executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./venv/bin/python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
### 1. Interface Management
|
||||||
|
- **Launch**: Verify the app starts and lists your network interfaces.
|
||||||
|
- **Details**: Select an interface to see its IP, MAC, and status.
|
||||||
|
|
||||||
|
### 2. IP Configuration & Calculator
|
||||||
|
- **Static IP**: Go to "IP Configuration". Select an interface, choose "Static", enter an IP (e.g., `192.168.99.1`) and CIDR (`24`). Click "Apply". Verify with `ip addr` in a terminal.
|
||||||
|
- **Calculator**: Enter an IP and CIDR in the calculator section. Click "Calculate" to see the Netmask and Broadcast address.
|
||||||
|
|
||||||
|
### 3. VLAN & Bridging
|
||||||
|
- **Wired Bridge**:
|
||||||
|
1. Go to "VLAN / Bridge".
|
||||||
|
2. Select your main Ethernet (Trunk) and a secondary Ethernet (Access).
|
||||||
|
3. Enter a VLAN ID (e.g., `35`).
|
||||||
|
4. Click "Create Bridge".
|
||||||
|
5. Verify connectivity on the secondary interface.
|
||||||
|
- **Wi-Fi AP**:
|
||||||
|
1. Select a Wireless interface.
|
||||||
|
2. Enter SSID and Password.
|
||||||
|
3. Select the bridge you just created (e.g., `br35`).
|
||||||
|
4. Click "Start Wi-Fi AP".
|
||||||
|
- **VLAN Scanner**:
|
||||||
|
1. In "VLAN / Bridge", look for "Passive VLAN Scanner".
|
||||||
|
2. Select an interface.
|
||||||
|
3. Click "Start Scan".
|
||||||
|
4. Wait 10 seconds. The table will show any VLAN tags detected on the wire.
|
||||||
|
|
||||||
|
### 4. Network Scanner
|
||||||
|
- Go to "Scanner".
|
||||||
|
- Enter a range (e.g., `192.168.1.0/24`).
|
||||||
|
- Click "Scan Network".
|
||||||
|
- Verify the list of devices appears.
|
||||||
|
|
||||||
|
### 5. Packet Sniffer
|
||||||
|
- Go to "Sniffer".
|
||||||
|
- Select an interface (e.g., the bridge `br35`).
|
||||||
|
- Click "Start Sniffing".
|
||||||
|
- Generate traffic (ping, browse).
|
||||||
|
- Verify packets appear in the table.
|
||||||
|
|
||||||
|
### 6. Reporting
|
||||||
|
- Click "Generate Report" in the toolbar.
|
||||||
|
- Save the HTML file.
|
||||||
|
- Open it in a browser to view the summary of interfaces, scan results, and captured packets.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **"No module named..."**: Ensure you are running inside the venv: `sudo ./venv/bin/python3 main.py`.
|
||||||
|
- **"xcb plugin error"**: Install `libxcb-cursor0`.
|
||||||
|
- **"Permission denied"**: Ensure you are running with `sudo`.
|
||||||
|
- **NetworkManager errors**: Ensure `nmcli` is installed (`sudo apt install network-manager`).
|
||||||
Reference in New Issue
Block a user