Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions wifite/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ class Configuration:
# System check mode
syscheck = None

@classmethod
def load_manufacturers(cls):
"""Lazy-load OUI manufacturer database on first access."""
if cls._manufacturers_loaded:
return
cls._manufacturers_loaded = True
Comment on lines +123 to +127

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Configuration.load_manufacturers() reads cls._manufacturers_loaded, but _manufacturers_loaded is not defined as a class attribute (it’s only set inside initialize). If load_manufacturers() is called before initialize(), this will raise AttributeError. Define _manufacturers_loaded = False at class scope (and consider using getattr(cls, '_manufacturers_loaded', False) for extra safety).

Copilot uses AI. Check for mistakes.
if os.path.isfile('/usr/share/ieee-data/oui.txt'):
mfr_file = '/usr/share/ieee-data/oui.txt'
else:
mfr_file = 'ieee-oui.txt'
if os.path.exists(mfr_file):
cls.manufacturers = {}
with open(mfr_file, 'r', encoding='utf-8') as f:
for line in f:
if not re.match(r'^\w', line):
continue
line = line.replace('(hex)', '').replace('(base 16)', '')
fields = line.split()
if len(fields) >= 2:
cls.manufacturers[fields[0]] = ' '.join(fields[1:]).rstrip('.')

@classmethod
def initialize(cls, load_interface=True):
"""
Expand Down Expand Up @@ -249,22 +270,9 @@ def initialize(cls, load_interface=True):
cls.wordlists = [wlist]
break

if os.path.isfile('/usr/share/ieee-data/oui.txt'):
manufacturers = '/usr/share/ieee-data/oui.txt'
else:
manufacturers = 'ieee-oui.txt'

if os.path.exists(manufacturers):
cls.manufacturers = {}
with open(manufacturers, "r", encoding='utf-8') as f:
# Parse txt format into dict
for line in f:
if not re.match(r"^\w", line):
continue
line = line.replace('(hex)', '').replace('(base 16)', '')
fields = line.split()
if len(fields) >= 2:
cls.manufacturers[fields[0]] = " ".join(fields[1:]).rstrip('.')
# Manufacturers database is lazy-loaded on first access via load_manufacturers()
cls.manufacturers = None
cls._manufacturers_loaded = False

# WPS variables
cls.wps_filter = False # Only attack WPS networks
Expand Down
3 changes: 2 additions & 1 deletion wifite/model/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ def to_str(self, show_bssid=False, show_manufacturer=False):
bssid = Color.s('{D}%s{W}' % self.bssid) if show_bssid else ''
if show_manufacturer:
oui = ''.join(self.bssid.split(':')[:3])
self.manufacturer = Configuration.manufacturers.get(oui, "")
Configuration.load_manufacturers()
self.manufacturer = Configuration.manufacturers.get(oui, "") if Configuration.manufacturers else ""

max_oui_len = 21
mfg_name = self.manufacturer
Expand Down
51 changes: 27 additions & 24 deletions wifite/tools/airodump.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,23 +184,17 @@ def get_targets(self, old_targets=None, apply_filter=True, target_archives=None)
new_targets = Airodump.get_targets_from_csv(csv_filename)

# Check if one of the targets is also contained in the old_targets
# Use dict lookup (O(1)) instead of nested loop (O(n*m))
old_by_bssid = {t.bssid: t for t in old_targets}
for new_target in new_targets:
just_found = True
for old_target in old_targets:
# If the new_target is found in old_target copy attributes from old target
if old_target == new_target:
# Identify decloaked targets
if new_target.essid_known and not old_target.essid_known:
# We decloaked a target!
new_target.decloaked = True

old_target.transfer_info(new_target)
just_found = False
break

# If the new_target is not in old_targets, check target_archives
# and copy attributes from there
if just_found and new_target.bssid in target_archives:
old_target = old_by_bssid.get(new_target.bssid)
if old_target is not None:
# Identify decloaked targets
if new_target.essid_known and not old_target.essid_known:
new_target.decloaked = True
old_target.transfer_info(new_target)
elif new_target.bssid in target_archives:
Comment on lines +187 to +196

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

old_by_bssid = {t.bssid: t for t in old_targets} changes behavior if old_targets contains duplicate BSSIDs (possible because CSV parsing doesn’t dedupe). The previous nested loop copied from the first matching old_target; this dict keeps the last one. If you want to preserve previous semantics, build the dict with “first wins” (e.g., only set the key if it’s not already present).

Copilot uses AI. Check for mistakes.
# If the new_target is not in old_targets, check target_archives
target_archives[new_target.bssid].transfer_info(new_target)

# Check targets for WPS
Expand Down Expand Up @@ -237,18 +231,22 @@ def get_targets_from_csv(csv_filename):
targets2 = []
import csv

# Detect encoding from first 4KB sample to avoid reading entire file twice
try:
import chardet
with open(csv_filename, "rb") as rawdata:
encoding = chardet.detect(rawdata.read())['encoding'] or 'utf-8'
with open(csv_filename, 'rb') as rawdata:
encoding = chardet.detect(rawdata.read(4096))['encoding'] or 'utf-8'
except ImportError:
encoding = 'utf-8'

with open(csv_filename, 'r', encoding=encoding, errors='ignore') as csvopen:
lines = []
has_null = False
for line in csvopen:
if '\0' in line:
log_warning('Airodump', 'Null bytes found in CSV data, stripping them')
if not has_null:
log_warning('Airodump', 'Null bytes found in CSV data, stripping them')
has_null = True
line = line.replace('\0', '')
lines.append(line)

Expand All @@ -272,6 +270,12 @@ def get_targets_from_csv(csv_filename):
elif row[0].strip() == 'Station MAC':
# This is the 'header' for the list of Clients
hit_clients = True
# Build BSSID lookup dict for O(1) client-to-target matching
# Use first occurrence per BSSID to match original behavior
targets_by_bssid = {}
for t in targets2:
if t.bssid not in targets_by_bssid:
targets_by_bssid[t.bssid] = t
continue

if hit_clients:
Expand All @@ -286,11 +290,10 @@ def get_targets_from_csv(csv_filename):
# Ignore unassociated clients
continue

# Add this client to the appropriate Target
for t in targets2:
if t.bssid == client.bssid:
t.clients.append(client)
break
# Add this client to the appropriate Target (O(1) dict lookup)
target = targets_by_bssid.get(client.bssid)
if target:
target.clients.append(client)

else:
# The current row corresponds to a 'Target' (router)
Expand Down
3 changes: 2 additions & 1 deletion wifite/ui/scanner_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,8 @@ def _format_manufacturer(self, target) -> Text:

# Get OUI (first 3 octets of BSSID)
oui = ''.join(target.bssid.split(':')[:3])
manufacturer = Configuration.manufacturers.get(oui, "Unknown")
Configuration.load_manufacturers()
manufacturer = Configuration.manufacturers.get(oui, "Unknown") if Configuration.manufacturers else "Unknown"

# Truncate if too long
max_len = 20
Expand Down
1 change: 1 addition & 0 deletions wifite/ui/selector_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ def _format_manufacturer(self, target) -> Text:

# Get OUI (first 3 octets of BSSID)
oui = ''.join(target.bssid.split(':')[:3])
Configuration.load_manufacturers()
manufacturer = Configuration.manufacturers.get(oui, "Unknown") if Configuration.manufacturers else "Unknown"

# Truncate if too long
Expand Down
18 changes: 15 additions & 3 deletions wifite/util/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def __init__(self, command, devnull=False, stdout=PIPE, stderr=PIPE, cwd=None, b
log_warning('Process', 'Delaying process creation due to high FD usage')
if Configuration.verbose > 0:
Color.pl('{!} {O}Delaying process creation due to high FD usage{W}')
time.sleep(0.5) # Brief delay to allow cleanup to complete
time.sleep(0.1) # Brief delay to allow cleanup to complete

self.out = None
self.err = None
Expand Down Expand Up @@ -447,15 +447,27 @@ def cleanup_zombies():
except Exception:
pass

# Cache for FD count to avoid filesystem scan on every process creation
_fd_cache_time = 0
_fd_cache_value = -1
_FD_CACHE_TTL = 2.0 # seconds

@staticmethod
def get_open_fd_count():
"""Get current open file descriptor count from /proc/{pid}/fd"""
"""Get current open file descriptor count from /proc/{pid}/fd (cached with TTL)"""
now = time.time()
if now - Process._fd_cache_time < Process._FD_CACHE_TTL:
return Process._fd_cache_value
try:
proc_fd_dir = f'/proc/{os.getpid()}/fd'
if os.path.exists(proc_fd_dir):
return len(os.listdir(proc_fd_dir))
Process._fd_cache_value = len(os.listdir(proc_fd_dir))
Process._fd_cache_time = now
return Process._fd_cache_value
Comment on lines 456 to +466

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_open_fd_count() caches results for 2s, but check_fd_limit() calls it twice back-to-back (before and immediately after cleanup). With the TTL, the “after cleanup” call will return the cached pre-cleanup value, making freed/new_count wrong and potentially keeping the system in a “high FD usage” state for up to the TTL. Consider adding a force_refresh/use_cache parameter or explicitly invalidating the cache before the post-cleanup re-check.

Copilot uses AI. Check for mistakes.
except Exception:
pass
Process._fd_cache_value = -1
Process._fd_cache_time = now
return -1

@staticmethod
Expand Down
13 changes: 5 additions & 8 deletions wifite/util/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,6 @@ def find_targets(self):
if self.found_target():
return True # We found the target we want

if airodump.pid.poll() is not None:
return True # Airodump process died

# Update display based on mode
if self.use_tui and self.view:
self.view.update_targets(self.targets, airodump.decloaking)
Expand Down Expand Up @@ -561,13 +558,13 @@ def _cleanup_memory(self):

# 2. Limit target list size (keep strongest signals)
if len(self.targets) > self._max_targets:
# Sort by power (strongest first)
self.targets.sort(key=lambda x: x.power, reverse=True)
import heapq
removed_count = len(self.targets) - self._max_targets
self.targets = self.targets[:self._max_targets]

# Use heapq.nlargest: O(n log k) vs O(n log n) for full sort
self.targets = heapq.nlargest(self._max_targets, self.targets, key=lambda x: x.power)

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heapq.nlargest() is not stable for ties, so when multiple targets share the same power, their relative order may become non-deterministic compared to the previous stable list.sort(). If target ordering is user-visible (and affects selection indices), consider adding a deterministic tie-breaker (e.g., (power, bssid)), or keep the stable sort when order matters.

Suggested change
self.targets = heapq.nlargest(self._max_targets, self.targets, key=lambda x: x.power)
# Add deterministic tie-breaker (BSSID) to avoid non-deterministic ordering for equal power
self.targets = heapq.nlargest(
self._max_targets,
self.targets,
key=lambda x: (x.power, getattr(x, 'bssid', ''))
)

Copilot uses AI. Check for mistakes.

if Configuration.verbose > 1:
Color.pl('{!} {O}Trimmed %d weak targets (limit: %d){W}' %
Color.pl('{!} {O}Trimmed %d weak targets (limit: %d){W}' %
(removed_count, self._max_targets))

# 3. Clean up old archived targets with time-based expiration
Expand Down
Loading