IP Scan
Windows LAN inventory & port scanner with a unique, frameless UI. Detect devices, IP/MAC, vendor, open ports, and SMB shares—fast.
IP Scan Source
import sys, os, re, json, csv, ipaddress, socket, subprocess, webbrowser, math
import concurrent.futures as futures
from typing import List, Dict, Any, Optional, Tuple
from PySide6.QtCore import Qt, QPoint, QRect, QSize, QThread, Signal, Slot
from PySide6.QtGui import QPainter, QColor, QPolygon, QRegion, QFont, QAction, QCursor, QIcon
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLayout, QSizePolicy, QMenu, QLabel, QLineEdit, QCheckBox, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QProgressBar, QFileDialog, QGraphicsDropShadowEffect, QMessageBox, QToolButton, QToolTip, QComboBox
try:
from mac_vendor_lookup import MacLookup
_MAC_LOOKUP = MacLookup()
except Exception:
_MAC_LOOKUP = None
try:
from smb.SMBConnection import SMBConnection
_HAS_SMB = True
except Exception:
_HAS_SMB = False
IS_WIN = os.name == "nt"
IS_MAC = sys.platform == "darwin"
IS_LIN = sys.platform.startswith("linux")
COMMON_PORTS: List[int] = [22,23,53,80,88,123,135,137,138,139,161,389,445,515,587,631,873,902,9100,1433,3306,3389,5000,5432,5900,8008,8080,8443]
def run_cmd(cmd: List[str], timeout: float = 5.0) -> str:
try:
creationflags = 0
startupinfo = None
if IS_WIN:
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
try:
si = subprocess.STARTUPINFO()
si.dwFlags |= getattr(subprocess, "STARTF_USESHOWWINDOW", 1)
startupinfo = si
except Exception:
startupinfo = None
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout, shell=False, creationflags=creationflags, startupinfo=startupinfo)
return ((p.stdout or "") + (p.stderr or "")) or ""
except Exception:
return ""
def ping_host(ip: str, timeout_ms: int = 400) -> bool:
if IS_WIN:
out = run_cmd(["ping", "-n", "1", "-w", str(timeout_ms), ip], timeout=(timeout_ms/1000.0 + 1.0))
return "TTL=" in out
if IS_LIN:
secs = max(1, int(math.ceil(timeout_ms/1000.0)))
out = run_cmd(["ping", "-c", "1", "-W", str(secs), ip], timeout=secs+1)
return "1 received" in out or "bytes from" in out
if IS_MAC:
ms = str(max(200, timeout_ms))
out = run_cmd(["ping", "-c", "1", "-W", ms, ip], timeout=(int(ms)/1000.0 + 1.0))
return "1 packets received" in out or "bytes from" in out
return False
def tcp_probe_up(ip: str, ports: List[int], timeout: float) -> bool:
for p in ports:
try:
with socket.create_connection((ip, p), timeout=timeout):
return True
except Exception:
continue
return False
def udp_nudge(ip: str) -> None:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(0.2); s.sendto(b"\x00", (ip, 9)); s.close()
except Exception:
pass
def get_mac_from_arp(ip: str) -> Optional[str]:
if IS_WIN:
out = run_cmd(["arp", "-a", ip], timeout=2)
m = re.search(r"([0-9A-Fa-f]{2}[-:]){5}[0-9A-Fa-f]{2}", out)
return m.group(0).lower().replace(":", "-") if m else None
if IS_LIN:
out = run_cmd(["ip", "neigh", "show", ip], timeout=2) or run_cmd(["arp", "-n", ip], timeout=2)
m = re.search(r"lladdr\s+([0-9a-f:]{17})", out, re.I) or re.search(r"([0-9a-f]{2}(:[0-9a-f]{2}){5})", out, re.I)
return m.group(1).lower().replace(":", "-") if m else None
if IS_MAC:
out = run_cmd(["arp", "-n", ip], timeout=2)
m = re.search(r"at\s+([0-9a-f:]{17})", out, re.I)
return m.group(1).lower().replace(":", "-") if m else None
return None
def mac_to_vendor(mac: str) -> Optional[str]:
if not mac or _MAC_LOOKUP is None:
return None
try:
return _MAC_LOOKUP.lookup(mac)
except Exception:
return None
def reverse_dns(ip: str, timeout: float = 1.0) -> Optional[str]:
with futures.ThreadPoolExecutor(max_workers=1) as ex:
fut = ex.submit(lambda: socket.gethostbyaddr(ip)[0] if True else None)
try:
return fut.result(timeout=timeout)
except Exception:
return None
def netbios_name(ip: str, timeout: float = 2.0) -> Optional[str]:
if not IS_WIN:
return None
out = run_cmd(["nbtstat", "-A", ip], timeout=timeout)
for line in out.splitlines():
if "<00>" in line and "UNIQUE" in line:
nm = line.split()[0].strip()
if nm and nm != "Name":
return nm
return None
def port_is_open(ip: str, port: int, timeout: float) -> bool:
try:
with socket.create_connection((ip, port), timeout=timeout):
return True
except Exception:
return False
def scan_ports(ip: str, ports: List[int], timeout: float, max_workers: int) -> List[int]:
if not ports:
return []
out: List[int] = []
with futures.ThreadPoolExecutor(max_workers=max_workers) as ex:
mapping = {ex.submit(port_is_open, ip, p, timeout): p for p in ports}
for f in futures.as_completed(mapping):
p = mapping[f]
try:
if f.result():
out.append(p)
except Exception:
pass
return sorted(out)
def enumerate_smb_shares(ip: str, timeout: float = 2.5) -> List[str]:
if not _HAS_SMB:
return []
try:
conn = SMBConnection(username="", password="", my_name="LANINV", remote_name=ip, use_ntlm_v2=True, is_direct_tcp=True)
ok = conn.connect(ip, 445, timeout=timeout)
shares: List[str] = []
if ok:
try:
for s in conn.listShares(timeout=timeout):
name = getattr(s, "name", "") or ""
if name and not (name.endswith("$") and name.lower() not in ("print$",)):
shares.append(name)
except Exception:
pass
try:
conn.close()
except Exception:
pass
return shares
except Exception:
return []
def detect_printer_like(open_ports: List[int]) -> bool:
return any(p in {9100, 515, 631} for p in open_ports)
def parse_win_networks() -> List[Dict[str,str]]:
out = run_cmd(["ipconfig"], timeout=5)
blocks = re.split(r"\r?\n\r?\n", out)
nets: List[Dict[str, str]] = []
for b in blocks:
if "Media disconnected" in b:
continue
header_m = re.search(r"^(.*adapter .*?):", b, flags=re.IGNORECASE | re.MULTILINE)
name = header_m.group(1).strip() if header_m else "Interface"
ipv4_m = re.search(r"IPv4 Address[^\:]*:\s*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", b)
mask_m = re.search(r"Subnet Mask[^\:]*:\s*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", b)
gw_m = re.search(r"Default Gateway[^\:]*:\s*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)", b)
if ipv4_m and mask_m:
ip = ipv4_m.group(1); mask = mask_m.group(1); gw = gw_m.group(1) if gw_m else ""
try:
net = ipaddress.IPv4Network((ip, mask), strict=False)
except Exception:
continue
nets.append({"name": name, "ip": ip, "mask": mask, "gateway": gw, "cidr": str(net)})
return nets
def parse_lin_networks() -> List[Dict[str,str]]:
a = run_cmd(["ip", "-o", "-f", "inet", "addr", "show"], timeout=5)
r = run_cmd(["ip", "-4", "route", "show", "default"], timeout=5)
gws = {}
for line in r.splitlines():
m = re.search(r"default via ([0-9.]+) dev (\S+)", line)
if m:
gws[m.group(2)] = m.group(1)
nets: List[Dict[str,str]] = []
for line in a.splitlines():
m = re.search(r"\d+:\s+(\S+)\s+inet\s+([0-9.]+)/(\d+)", line)
if not m:
continue
iface, ip, pref = m.group(1), m.group(2), int(m.group(3))
try:
net = ipaddress.IPv4Network(f"{ip}/{pref}", strict=False)
except Exception:
continue
gw = gws.get(iface, "")
nets.append({"name": iface, "ip": ip, "mask": str(ipaddress.IPv4Network(f"0.0.0.0/{pref}").netmask), "gateway": gw, "cidr": str(net)})
return nets
def parse_mac_networks() -> List[Dict[str,str]]:
f = run_cmd(["ifconfig"], timeout=5)
rd = run_cmd(["route", "-n", "get", "default"], timeout=5)
gw = ""
iface = ""
for line in rd.splitlines():
if line.strip().startswith("gateway:"):
gw = line.split()[-1].strip()
if line.strip().startswith("interface:"):
iface = line.split()[-1].strip()
nets: List[Dict[str,str]] = []
cur = None
for line in f.splitlines():
if not line.startswith("\t") and ":" in line:
cur = line.split(":")[0].strip()
continue
if cur and "inet " in line:
m = re.search(r"inet\s+([0-9.]+)\s+netmask\s+0x([0-9a-fA-F]+)", line)
if not m:
continue
ip = m.group(1)
nm_hex = int(m.group(2), 16)
mask = ".".join(str((nm_hex >> (i*8)) & 0xFF) for i in [3,2,1,0])
try:
net = ipaddress.IPv4Network((ip, mask), strict=False)
except Exception:
continue
nets.append({"name": cur, "ip": ip, "mask": mask, "gateway": gw if cur == iface and gw else "", "cidr": str(net)})
return nets
def parse_all_networks() -> List[Dict[str,str]]:
try:
if IS_WIN:
nets = parse_win_networks()
elif IS_LIN:
nets = parse_lin_networks()
elif IS_MAC:
nets = parse_mac_networks()
else:
nets = []
except Exception:
nets = []
unique: List[Dict[str, str]] = []
seen = set()
for n in nets:
key = (n.get("cidr",""), n.get("gateway",""), n.get("name",""))
if key in seen or not n.get("cidr"):
continue
seen.add(key)
ipn = ipaddress.IPv4Network(n["cidr"], strict=False)
if ipn.num_addresses >= 2:
unique.append(n)
return unique
def expand_targets(input_text: Optional[str], dropdown_mode: str) -> List[ipaddress.IPv4Network]:
lst: List[ipaddress.IPv4Network] = []
nets = parse_all_networks()
if input_text:
parts = [p.strip() for p in input_text.split(",") if p.strip()]
for p in parts:
try:
lst.append(ipaddress.IPv4Network(p, strict=False))
except Exception:
pass
return lst
if dropdown_mode == "All adapters":
for n in nets:
try:
lst.append(ipaddress.IPv4Network(n["cidr"], strict=False))
except Exception:
pass
return lst
if nets:
pri = next((n for n in nets if n.get("gateway")), nets[0])
try:
lst.append(ipaddress.IPv4Network(pri["cidr"], strict=False))
except Exception:
pass
return lst
class FlowLayout(QLayout):
def __init__(self, parent=None, hspacing=10, vspacing=6, margins=(0,0,0,0)):
super().__init__(parent); self._items=[]; self._hspace=hspacing; self._vspace=vspacing; self.setContentsMargins(*margins)
def addItem(self, item): self._items.append(item)
def addWidget(self, w): super().addWidget(w)
def count(self): return len(self._items)
def itemAt(self, i): return self._items[i] if 0<=i right and line_h>0:
x = rect.x(); y = y + line_h + spaceY; nextX = x + wid.sizeHint().width() + spaceX; line_h = 0
if not testOnly: it.setGeometry(QRect(QPoint(x,y), wid.sizeHint()))
x = nextX; line_h = max(line_h, wid.sizeHint().height())
return y + line_h - rect.y()
class ScanWorker(QThread):
progress = Signal(int, int)
finished = Signal(list, str)
def __init__(self, network_cidrs: List[str], do_ports: bool, do_smb: bool, timeout: float, threads: int, ports: List[int]):
super().__init__(); self.network_cidrs=network_cidrs; self.do_ports=do_ports; self.do_smb=do_smb; self.timeout=timeout; self.threads=threads; self.ports=ports
def host_generator(self, nets: List[ipaddress.IPv4Network]) -> List[str]:
hosts: List[str] = []
for n in nets:
try:
hosts.extend([str(ip) for ip in n.hosts()])
except Exception:
pass
return hosts
def run(self):
try:
nets = [ipaddress.IPv4Network(c, strict=False) for c in self.network_cidrs if c]
if not nets:
raise RuntimeError("No IPv4 network to scan.")
except Exception as e:
self.finished.emit([], f"{e}")
return
ips = self.host_generator(nets)
total = len(ips)
if total == 0:
self.finished.emit([], "No hosts in selected network(s).")
return
live: List[str] = []
probe_ports = [80, 443, 22, 139, 445, 3389, 53]
def alive(ip):
if ping_host(ip, int(self.timeout*1000)):
return ip
udp_nudge(ip)
mac = get_mac_from_arp(ip)
if mac:
return ip
if tcp_probe_up(ip, probe_ports, self.timeout/2 if self.timeout>0.2 else 0.2):
return ip
udp_nudge(ip)
mac = get_mac_from_arp(ip)
if mac:
return ip
return None
with futures.ThreadPoolExecutor(max_workers=self.threads) as ex:
for i, res in enumerate(ex.map(alive, ips), start=1):
if res:
live.append(res)
self.progress.emit(i, total)
if not live:
self.finished.emit([], "")
return
def enrich_basic(ip):
h = reverse_dns(ip, timeout=1.0) or (netbios_name(ip, timeout=1.5) or "")
mac = get_mac_from_arp(ip) or ""
ven = mac_to_vendor(mac) or ""
return {"ip": ip, "hostname": h, "mac": mac, "vendor": ven, "open_ports": [], "shares": [], "printer_like": False}
with futures.ThreadPoolExecutor(max_workers=min(self.threads, 256)) as ex:
results = list(ex.map(enrich_basic, live))
if self.do_ports:
def do_ports(rec: Dict[str,Any]) -> Dict[str,Any]:
rec["open_ports"] = scan_ports(rec["ip"], self.ports, self.timeout, max_workers=min(256, len(self.ports) or 1))
rec["printer_like"] = detect_printer_like(rec["open_ports"])
return rec
with futures.ThreadPoolExecutor(max_workers=min(self.threads, 256)) as ex:
results = list(ex.map(do_ports, results))
if self.do_smb and _HAS_SMB:
def do_smb(rec: Dict[str,Any]) -> Dict[str,Any]:
if 445 in rec.get("open_ports", []):
rec["shares"] = enumerate_smb_shares(rec["ip"])[:25]
return rec
with futures.ThreadPoolExecutor(max_workers=64) as ex:
results = list(ex.map(do_smb, results))
self.finished.emit(results, "")
class HexWindow(QWidget):
def __init__(self):
super().__init__(None, Qt.FramelessWindowHint | Qt.Window)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.dragging = False
self.drag_offset = QPoint()
self.resize(1000, 660)
self.setMinimumSize(860, 560)
self.container = QWidget(self)
self.container.setObjectName("container")
self.container.setStyleSheet("""
#container { background: qlineargradient(x1:0,y1:0,x2:1,y2:1, stop:0 #0e1526, stop:1 #0a1020);
border:1px solid rgba(255,255,255,0.12); border-radius:28px; }
QLabel, QLineEdit, QCheckBox, QPushButton, QComboBox { color:#e5e7eb; font-size:14px; }
QCheckBox::indicator { width:16px; height:16px; }
QLineEdit, QComboBox { background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.18);
border-radius:10px; padding:8px 10px; }
QPushButton { background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.20);
border-radius:12px; padding:10px 16px; min-width:104px; }
QPushButton:hover { background:rgba(255,255,255,0.14); }
QToolButton { background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.20);
border-radius:10px; padding:6px 10px; }
QToolButton::menu-indicator { image: none; }
QTableWidget { background:rgba(255,255,255,0.04); gridline-color:rgba(255,255,255,0.18);
border:1px solid rgba(255,255,255,0.12); border-radius:12px; alternate-background-color:rgba(255,255,255,0.06); }
QHeaderView::section { background:rgba(255,255,255,0.12); padding:8px; border:0px; color:#ffffff; font-weight:700; }
QTableWidget::item { color:#e9eef8; }
QTableWidget::item:selected { background:rgba(59,130,246,0.35); color:#ffffff; }
QProgressBar { text-align:center; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.18);
border-radius:10px; color:#e5e7eb; }
QProgressBar::chunk { background:#3b82f6; border-radius:8px; }
""")
shadow = QGraphicsDropShadowEffect(self.container)
shadow.setBlurRadius(50)
shadow.setOffset(0, 20)
shadow.setColor(QColor(0,0,0,150))
self.container.setGraphicsEffect(shadow)
self.vbox = QVBoxLayout(self.container)
self.vbox.setContentsMargins(22, 22, 22, 22)
self.vbox.setSpacing(12)
top = QHBoxLayout()
top.setSpacing(10)
self.title = QLabel("IP Scan")
self.title.setStyleSheet("font-size:18px;font-weight:800;color:#e2e8f0;")
top.addWidget(self.title)
top.addStretch(1)
self.minBtn = QPushButton("—")
self.minBtn.setFixedSize(30, 30)
self.minBtn.setStyleSheet("QPushButton{border-radius:15px;background:#fbbf24;color:#0b1220;font-weight:900;min-width:30px;}")
self.minBtn.clicked.connect(self.showMinimized)
self.closeBtn = QPushButton("×")
self.closeBtn.setFixedSize(30, 30)
self.closeBtn.setStyleSheet("QPushButton{border-radius:15px;background:#ef4444;color:white;font-weight:900;min-width:30px;}")
self.closeBtn.clicked.connect(self.close)
top.addWidget(self.minBtn); top.addWidget(self.closeBtn)
self.vbox.addLayout(top)
ctl = FlowLayout(hspacing=10, vspacing=8, margins=(2,2,2,2))
self.netCombo = QComboBox(); self.netCombo.setMinimumWidth(280)
self.netInput = QLineEdit(); self.netInput.setPlaceholderText("Auto or enter CIDR(s), comma-separated"); self.netInput.setMinimumWidth(260); self.netInput.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.refreshBtn = QToolButton(); self.refreshBtn.setText("↻"); self.refreshBtn.setToolTip("Refresh adapters"); self.refreshBtn.setFixedWidth(36)
net_group = QWidget(); nh = QHBoxLayout(net_group); nh.setContentsMargins(0,0,0,0); nh.setSpacing(6)
nh.addWidget(self.netCombo); nh.addWidget(self.netInput); nh.addWidget(self.refreshBtn)
ctl.addWidget(self.wrap_labeled("Network", net_group))
self.doPorts = QCheckBox("Port scan"); self.doPorts.setChecked(True); ctl.addWidget(self.doPorts)
self.doSMB = QCheckBox("SMB shares"); self.doSMB.setChecked(True); ctl.addWidget(self.doSMB)
self.timeoutInput = QLineEdit(); self.timeoutInput.setFixedWidth(80); self.timeoutInput.setText("0.5"); ctl.addWidget(self.wrap_labeled("Timeout", self.timeoutInput))
self.threadInput = QLineEdit(); self.threadInput.setFixedWidth(80); self.threadInput.setText("400"); ctl.addWidget(self.wrap_labeled("Threads", self.threadInput))
self.scanBtn = QPushButton("Scan"); self.scanBtn.setMinimumWidth(110); ctl.addWidget(self.scanBtn)
self.exportBtn = QPushButton("Export CSV"); self.exportBtn.setMinimumWidth(130); self.exportBtn.setEnabled(False); ctl.addWidget(self.exportBtn)
ctl_host = QWidget(); ctl_host.setLayout(ctl); self.vbox.addWidget(ctl_host)
self.progress = QProgressBar(); self.progress.setRange(0,100); self.progress.setValue(0); self.vbox.addWidget(self.progress)
self.table = QTableWidget(0, 7); self.table.setAlternatingRowColors(True)
self.table.setHorizontalHeaderLabels(["IP","Hostname","MAC","Manufacturer","Open Ports","Printer","SMB Shares"])
hdr = self.table.horizontalHeader(); hdr.setSectionsClickable(False); hdr.setStretchLastSection(False)
hdr.setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive); hdr.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
for c in [2,3,4,5,6]: hdr.setSectionResizeMode(c, QHeaderView.ResizeMode.Interactive)
self.table.setColumnWidth(0,140); self.table.setColumnWidth(2,170); self.table.setColumnWidth(3,200); self.table.setColumnWidth(4,140); self.table.setColumnWidth(5,90); self.table.setColumnWidth(6,160)
self.table.verticalHeader().setDefaultSectionSize(32); self.table.verticalHeader().setVisible(False)
self.vbox.addWidget(self.table, 1)
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.on_table_context)
self.scanBtn.clicked.connect(self.start_scan); self.exportBtn.clicked.connect(self.export_csv)
self.refreshBtn.clicked.connect(self.populate_networks)
self.netCombo.currentIndexChanged.connect(self.on_network_selected)
self.results: List[Dict[str, Any]] = []
self.populate_networks()
def wrap_labeled(self, text: str, widget: QWidget) -> QWidget:
w = QWidget(); h = QHBoxLayout(w); h.setContentsMargins(0,0,0,0); h.setSpacing(6); lbl = QLabel(text); lbl.setStyleSheet("color:#cbd5e1;font-weight:700;"); h.addWidget(lbl); h.addWidget(widget); w.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed); return w
def resizeEvent(self, e):
super().resizeEvent(e); self.container.setGeometry(self.hex_inner_rect()); self.apply_hex_mask()
def hex_inner_rect(self)->QRect:
w,h=self.width(),self.height(); t=int(min(w,h)*0.18); ml=mr=t+22; mt=mb=22; return QRect(ml,mt,max(420,w-ml-mr),max(260,h-mt-mb))
def apply_hex_mask(self):
w,h=self.width(),self.height(); t=int(min(w,h)*0.18)
pts=[QPoint(t,0), QPoint(w-t,0), QPoint(w,h//2), QPoint(w-t,h), QPoint(t,h), QPoint(0,h//2)]
self.setMask(QRegion(QPolygon(pts)))
def paintEvent(self,e): p=QPainter(self); p.setRenderHint(QPainter.Antialiasing,True); p.end()
def mousePressEvent(self,e):
if e.button()==Qt.LeftButton: self.dragging=True; self.drag_offset=e.globalPosition().toPoint()-self.frameGeometry().topLeft(); e.accept()
def mouseMoveEvent(self,e):
if self.dragging and e.buttons()&Qt.LeftButton: self.move(e.globalPosition().toPoint()-self.drag_offset); e.accept()
def mouseReleaseEvent(self,e):
if e.button()==Qt.LeftButton: self.dragging=False; e.accept()
def populate_networks(self):
self.netCombo.blockSignals(True); self.netCombo.clear(); self.netCombo.addItem("Current (auto)", userData="Current (auto)"); self.netCombo.addItem("All adapters", userData="All adapters")
nets = parse_all_networks()
for n in nets:
label = f"{n['name']} — {n['cidr']}" + (f" via {n['gateway']}" if n.get('gateway') else " (no gw)")
self.netCombo.addItem(label, userData=n["cidr"])
self.netCombo.setCurrentIndex(0); self.netInput.clear(); self.netCombo.blockSignals(False)
@Slot(int)
def on_network_selected(self, idx: int):
data = self.netCombo.itemData(idx)
if data and data not in ("Current (auto)","All adapters"):
self.netInput.setText(str(data))
else:
self.netInput.clear()
def start_scan(self):
if hasattr(self,"worker") and self.worker.isRunning(): return
mode = self.netCombo.currentText()
cidr_text = self.netInput.text().strip() or None
try: timeout=float(self.timeoutInput.text().strip())
except Exception: timeout=0.5
try: threads=int(self.threadInput.text().strip())
except Exception: threads=400
nets = expand_targets(cidr_text, "All adapters" if mode.startswith("All adapters") else ("Current (auto)" if mode.startswith("Current") else mode))
cidrs = [str(n) for n in nets]
if not cidrs:
QMessageBox.warning(self,"Network","No valid networks to scan."); return
self.table.setRowCount(0); self.results=[]; self.exportBtn.setEnabled(False); self.progress.setRange(0,0)
ports = COMMON_PORTS[:] if self.doPorts.isChecked() else []
self.worker = ScanWorker(cidrs, self.doPorts.isChecked(), self.doSMB.isChecked(), timeout, threads, ports)
self.worker.progress.connect(self.on_progress); self.worker.finished.connect(self.on_finished); self.worker.start()
@Slot(int,int)
def on_progress(self, cur: int, total: int):
if total>0: self.progress.setRange(0,total); self.progress.setValue(cur)
@Slot(list,str)
def on_finished(self, results: List[Dict[str,Any]], error: str):
self.progress.setRange(0,1); self.progress.setValue(1)
if error: QMessageBox.warning(self,"Scan error",error)
self.results = results or []; self.populate_table(self.results); self.exportBtn.setEnabled(bool(self.results))
def _ports_button(self, ports: List[int], ip: str):
if not ports:
lbl = QLabel("—"); lbl.setStyleSheet("color:#94a3b8;"); return lbl
btn = QToolButton(); btn.setText(f"{len(ports)} ports ▾")
menu = QMenu(btn)
for p in ports:
a = QAction(str(p), menu); a.setEnabled(False); menu.addAction(a)
menu.addSeparator()
a_copy = QAction("Copy ports", menu); a_copy.triggered.connect(lambda: self.copy_to_clipboard(" ".join(map(str, ports)), "Ports copied")); menu.addAction(a_copy)
if 443 in ports:
a_https = QAction("Open https://"+ip, menu); a_https.triggered.connect(lambda: webbrowser.open(f"https://{ip}")); menu.addAction(a_https)
if 80 in ports:
a_http = QAction("Open http://"+ip, menu); a_http.triggered.connect(lambda: webbrowser.open(f"http://{ip}")); menu.addAction(a_http)
btn.setMenu(menu); btn.setPopupMode(QToolButton.InstantPopup)
return btn
def _shares_button(self, shares: List[str]):
if not shares:
lbl = QLabel("—"); lbl.setStyleSheet("color:#94a3b8;"); return lbl
btn = QToolButton(); btn.setText(f"{len(shares)} shares ▾")
menu = QMenu(btn)
for s in shares:
a = QAction(s, menu); a.setEnabled(False); menu.addAction(a)
menu.addSeparator()
a_copy = QAction("Copy shares", menu); a_copy.triggered.connect(lambda: self.copy_to_clipboard("; ".join(shares), "Shares copied")); menu.addAction(a_copy)
btn.setMenu(menu); btn.setPopupMode(QToolButton.InstantPopup)
return btn
def populate_table(self, results: List[Dict[str,Any]]):
self.table.setRowCount(0)
for r in results:
row = self.table.rowCount(); self.table.insertRow(row)
vals = [r.get("ip",""), r.get("hostname",""), r.get("mac",""), r.get("vendor","")]
for col, val in enumerate(vals):
item = QTableWidgetItem(val)
if col in (0,2):
try: item.setFont(QFont("Consolas",10))
except Exception: pass
self.table.setItem(row, col, item)
self.table.setCellWidget(row,4, self._ports_button(r.get("open_ports",[]), r.get("ip","")))
self.table.setItem(row,5, QTableWidgetItem("yes" if r.get("printer_like") else ""))
self.table.setCellWidget(row,6, self._shares_button(r.get("shares",[])))
def on_table_context(self, pos):
idx = self.table.indexAt(pos)
if not idx.isValid(): return
row = idx.row()
if not (0 <= row < len(self.results)): return
rec = self.results[row]
ip = rec.get("ip",""); host=rec.get("hostname",""); mac=rec.get("mac",""); ven=rec.get("vendor","")
menu = QMenu(self.table)
menu.addAction("Copy IP", lambda: self.copy_to_clipboard(ip, "IP copied"))
menu.addAction("Copy Hostname", lambda: self.copy_to_clipboard(host, "Hostname copied"))
menu.addAction("Copy MAC", lambda: self.copy_to_clipboard(mac, "MAC copied"))
menu.addAction("Copy Manufacturer", lambda: self.copy_to_clipboard(ven, "Manufacturer copied"))
menu.addSeparator()
menu.addAction("Copy row (JSON)", lambda: self.copy_to_clipboard(json.dumps(rec, indent=2), "Row JSON copied"))
menu.addSeparator()
menu.addAction("Open HTTPS", lambda: webbrowser.open(f"https://{ip}"))
menu.addAction("Open HTTP", lambda: webbrowser.open(f"http://{ip}"))
menu.exec(self.table.viewport().mapToGlobal(pos))
def copy_to_clipboard(self, text: str, toast: str):
QApplication.clipboard().setText(text or ""); QToolTip.showText(QCursor.pos(), toast, self.table, timeout=1000)
def export_csv(self):
if not self.results: return
path,_ = QFileDialog.getSaveFileName(self, "Save CSV", "lan_inventory_gui.csv", "CSV Files (*.csv)")
if not path: return
try:
with open(path, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f); w.writerow(["IP","Hostname","MAC","Manufacturer","Open Ports","Printer-like","SMB Shares"])
for r in self.results:
w.writerow([
r.get("ip",""), r.get("hostname",""), r.get("mac",""), r.get("vendor",""),
" ".join(map(str, r.get("open_ports",[]))) if r.get("open_ports") else "",
"yes" if r.get("printer_like") else "",
"; ".join(r.get("shares",[])) if r.get("shares") else ""
])
QMessageBox.information(self, "Export", f"Saved: {path}")
except Exception as e:
QMessageBox.warning(self, "Export error", str(e))
if __name__ == "__main__":
app = QApplication(sys.argv)
w = HexWindow()
w.setWindowIcon(QIcon())
w.setWindowTitle("IP Scan")
w.show()
sys.exit(app.exec())
What it does
Auto-detects your active subnet and finds live hosts quickly.
Scans common ports (RDP/HTTP/HTTPS/SMB/etc) and lists SMB shares when accessible.
Pulls MAC via ARP, maps to manufacturer (OUI), and attempts reverse DNS for hostnames.
Copy IP/MAC/hostname/vendor or open device via HTTP/HTTPS directly.
Quickly switch between adapters/VLANs/VPNs; “Current (auto)” stays the default.
Works out of the box using ping+ARP; optional SMB and vendor lookup supported.
How it works
No install needed. Just launch the EXE and click Scan.
Use the Network dropdown to target any detected adapter/subnet or keep “Current (auto)”.
See IP, hostname, MAC, vendor, open ports, and SMB shares. Export to CSV anytime.
FAQ
Does it require admin rights?
No. Standard user works. Some features (like advanced ARP sweep) may require admin if you enable them in future builds.
Will my antivirus flag it?
Port scanners can trigger heuristics. If you see a warning, verify the SHA-256 shown above and allow/restore.
Does it phone home?
No telemetry. Optional MAC vendor lookup uses a local OUI DB when installed; SMB listing is LAN-only.
Can it scan other VLANs?
Yes, if they’re routed/accessible from your host. The network dropdown shows local adapter subnets; routed ranges can still be scanned by entering a CIDR manually.
Quick actions
Copy IP / Hostname / MAC / Vendor, or open HTTP/HTTPS.
Use the per-row dropdowns to view/copy open ports and SMB shares.
Save results to CSV for tickets, audits, or inventory.
System requirements
- Windows 10 or later
- ~48 MB free space
- No admin required (default features)