Initial commit: Passive Network Scanner

This commit is contained in:
Mert ALTINBAS 2026-05-18 21:04:35 +02:00
commit 5db74fb495
3 changed files with 166 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
scanner.db
__pycache__/
.venv/

162
passive_sniff.py Normal file
View file

@ -0,0 +1,162 @@
from scapy.all import sniff, ARP, DHCP, DNS, DNSQR, UDP, IP, Ether, BOOTP
import urllib.request
import threading
import sqlite3
import datetime
import time
devices = {}
mac_vendors = {}
api_lock = threading.Lock()
# ----- DATABASE FUNCTIONS -----
def init_db():
# Initialize SQLite database and create tables for devices, DNS queries, and services
conn = sqlite3.connect('scanner.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS devices (mac TEXT PRIMARY KEY, ip TEXT, vendor TEXT, hostname TEXT, last_seen TEXT)''')
c.execute('''CREATE TABLE IF NOT EXISTS dns_queries (ip TEXT, domain TEXT, timestamp TEXT)''')
c.execute('''CREATE TABLE IF NOT EXISTS services (ip TEXT, service_name TEXT, timestamp TEXT)''')
conn.commit()
conn.close()
def log_device(mac, ip, vendor, hostname):
# Insert or update device information in the database
conn = sqlite3.connect('scanner.db')
c = conn.cursor()
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
c.execute('''INSERT INTO devices (mac, ip, vendor, hostname, last_seen)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(mac) DO UPDATE SET
ip=excluded.ip, vendor=excluded.vendor, hostname=excluded.hostname, last_seen=excluded.last_seen''',
(mac, ip, vendor, hostname, now))
conn.commit()
conn.close()
def log_dns(ip, domain):
# Log DNS query from device to database
conn = sqlite3.connect('scanner.db')
c = conn.cursor()
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
c.execute("INSERT INTO dns_queries (ip, domain, timestamp) VALUES (?, ?, ?)", (ip, domain, now))
conn.commit()
conn.close()
def log_service(ip, service_name):
# Log discovered service (mDNS) to database
conn = sqlite3.connect('scanner.db')
c = conn.cursor()
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
c.execute("INSERT INTO services (ip, service_name, timestamp) VALUES (?, ?, ?)", (ip, service_name, now))
conn.commit()
conn.close()
# ----- NETWORK LISTENING FUNCTIONS -----
def is_random_mac(mac):
# Check if MAC is locally administered (random/spoofed)
# Get first byte and convert to integer
first_byte = int(mac[:2], 16)
# Check if second bit (Locally Administered bit) is 1
return (first_byte & 2) == 2
def get_mac_vendor(mac, ip):
# Fetch vendor name from API using MAC prefix with thread-safe locking
prefix = mac[:8].upper()
if is_random_mac(mac):
vendor = "Random/Hidden MAC"
mac_vendors[prefix] = vendor
elif prefix not in mac_vendors:
# LOCK START: Only 1 thread can query API at a time, others wait
with api_lock:
try:
# If another thread already resolved this vendor while waiting, skip the API call
if prefix in mac_vendors:
vendor = mac_vendors[prefix]
else:
url = f"https://api.macvendors.com/{mac}"
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
response = urllib.request.urlopen(req, timeout=3)
vendor = response.read().decode('utf-8')
mac_vendors[prefix] = vendor
time.sleep(1.2) # Sleep to respect API rate limit
except urllib.error.HTTPError as e:
if e.code == 429:
vendor = "API Limit"
else:
vendor = "Unknown"
mac_vendors[prefix] = vendor
except Exception:
vendor = "Unknown"
mac_vendors[prefix] = vendor
# LOCK END
else:
vendor = mac_vendors[prefix]
print(f"[+] New Device: {ip} ({mac}) -> Vendor: {vendor}")
# FIX: Guard against race condition — devices[mac] may not exist if called unexpectedly
device = devices.get(mac)
hostname = device.get('hostname', '') if device else ''
log_device(mac, ip, vendor, hostname)
def process_packet(packet):
# Parse network packets and extract device info, DNS queries, and services
if packet.haslayer(Ether) and packet.haslayer(IP):
mac = packet[Ether].src
ip = packet[IP].src
# If this MAC hasn't been recorded before, capture it immediately
if mac not in devices:
devices[mac] = {'ip': ip, 'hostname': ''}
threading.Thread(target=get_mac_vendor, args=(mac, ip)).start()
# 1. ARP Detection (ARP packets don't contain IP layer, so we keep this separately)
if packet.haslayer(ARP) and packet[ARP].op == 1:
mac = packet[ARP].hwsrc
ip = packet[ARP].psrc
if mac not in devices:
devices[mac] = {'ip': ip, 'hostname': ''}
threading.Thread(target=get_mac_vendor, args=(mac, ip)).start()
# 2. DHCP Detection (to extract hostname)
if packet.haslayer(DHCP) and packet.haslayer(BOOTP):
# op == 1 means: "This is a device IP request (Discover/Request)", not a server response
if packet[BOOTP].op == 1:
mac = packet[Ether].src
for opt in packet[DHCP].options:
if isinstance(opt, tuple) and opt[0] == 'hostname':
hostname = opt[1].decode('utf-8')
if mac in devices:
devices[mac]['hostname'] = hostname
# FIX: Use consistent "Unknown" fallback (was "Bilinmiyor" in Turkish)
log_device(mac, devices[mac]['ip'], mac_vendors.get(mac[:8].upper(), "Unknown"), hostname)
print(f"[*] Device Name: {mac} -> {hostname}")
# 3. DNS Detection (Internet sites)
if packet.haslayer(DNSQR) and packet.haslayer(IP):
ip_src = packet[IP].src
query = packet[DNSQR].qname.decode('utf-8')
if not query.endswith('.local.'):
print(f"[>] {ip_src} -> {query}")
log_dns(ip_src, query)
# 4. mDNS Detection (smart device services)
if packet.haslayer(UDP) and packet[UDP].dport == 5353 and packet.haslayer(DNS) and packet.haslayer(IP):
ip_src = packet[IP].src
if packet[DNS].qd:
qname = packet[DNS].qd.qname.decode('utf-8')
if "_tcp" in qname or "_udp" in qname or "apple" in qname:
print(f"[~] Service: {ip_src} -> {qname}")
log_service(ip_src, qname)
def start_sniffer():
# Initialize database and begin packet sniffing on the network
print("Initializing database...")
init_db()
print("Advanced Listening Active (Any packet will be immediately added to MAC device list)...")
sniff(prn=process_packet, filter="arp or (udp port 53) or (udp port 5353) or (udp port 67) or (udp port 68)", store=0)
if __name__ == "__main__":
start_sniffer()

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
scapy==2.7.0