Skip to content

Commit f0127b1

Browse files
committed
scanner/target: align output
1 parent 71c9428 commit f0127b1

4 files changed

Lines changed: 608 additions & 15 deletions

File tree

wifite/model/target.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,12 @@ def to_str(self, show_bssid=False, show_manufacturer=False):
265265
else:
266266
display_encryption = Color.s('{W}%s' % display_encryption) # White for others
267267

268-
encryption_display_string = f"{display_encryption}{auth_suffix}".ljust(7) # Ensure consistent length
268+
# Calculate padding for ENCR column based on its content length
269+
# Max length of ENCR (e.g. WPA2-P) is 6. OPN is 3.
270+
# Pad with spaces to ensure alignment
271+
encryption_padding = " " * (5 - len(self.primary_encryption + self.primary_authentication)) # Max length of WPA2-P is 6 (WPA2 + -P)
272+
encryption_display_string = f"{display_encryption}{auth_suffix}{encryption_padding}"
273+
269274

270275
power = f'{str(self.power).rjust(3)}db'
271276
if self.power > 50:
@@ -291,7 +296,7 @@ def to_str(self, show_bssid=False, show_manufacturer=False):
291296
if len(self.clients) > 0:
292297
clients = Color.s('{G} ' + str(len(self.clients)))
293298

294-
result = f'{essid} {bssid}{manufacturer}{channel} {encryption_display_string} {power} {wps} {clients}'
299+
result = f'{essid} {bssid}{manufacturer}{channel} {encryption_display_string} {power} {wps} {clients}'
295300

296301
result += Color.s('{W}')
297302
return result

wifite/model/target_backup.py

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
from ..util.color import Color
5+
from ..config import Configuration
6+
7+
import re
8+
9+
10+
class WPSState:
11+
NONE, UNLOCKED, LOCKED, UNKNOWN = list(range(4))
12+
13+
14+
class ArchivedTarget(object):
15+
"""
16+
Holds information between scans from a previously found target
17+
"""
18+
19+
def __init__(self, target):
20+
self.bssid = target.bssid
21+
self.channel = target.channel
22+
self.decloaked = target.decloaked
23+
self.attacked = target.attacked
24+
self.essid = target.essid
25+
self.essid_known = target.essid_known
26+
self.essid_len = target.essid_len
27+
28+
def transfer_info(self, other):
29+
"""
30+
Helper function to transfer relevant fields into another Target or ArchivedTarget
31+
"""
32+
other.attacked = self.attacked
33+
34+
if self.essid_known and other.essid_known:
35+
other.decloaked = self.decloaked
36+
37+
if not other.essid_known:
38+
other.decloaked = self.decloaked
39+
other.essid = self.essid
40+
other.essid_known = self.essid_known
41+
other.essid_len = self.essid_len
42+
43+
def __eq__(self, other):
44+
# Check if the other class type is either ArchivedTarget or Target
45+
return isinstance(other, (self.__class__, Target)) and self.bssid == other.bssid
46+
47+
48+
class Target(object):
49+
"""
50+
Holds details for a 'Target' aka Access Point (e.g. router).
51+
"""
52+
53+
def __init__(self, fields):
54+
"""
55+
Initializes & stores target info based on fields.
56+
Args:
57+
Fields - List of strings
58+
INDEX KEY EXAMPLE
59+
0 BSSID (00:1D:D5:9B:11:00)
60+
1 First time seen (2015-05-27 19:28:43)
61+
2 Last time seen (2015-05-27 19:28:46)
62+
3 channel (6)
63+
4 Speed (54)
64+
5 Privacy (WPA2 OWE)
65+
6 Cipher (CCMP TKIP)
66+
7 Authentication (PSK SAE)
67+
8 Power (-62)
68+
9 beacons (2)
69+
10 # IV (0)
70+
11 LAN IP (0. 0. 0. 0)
71+
12 ID-length (9)
72+
13 ESSID (HOME-ABCD)
73+
14 Key ()
74+
"""
75+
self.manufacturer = None
76+
self.wps = WPSState.NONE
77+
self.bssid = fields[0].strip()
78+
self.channel = fields[3].strip()
79+
self.encryption = fields[5].strip() # Contains encryption type(s) like "WPA2 WPA3 OWE"
80+
self.authentication = fields[7].strip() # Contains auth type(s) like "PSK SAE MGT"
81+
82+
# Determine primary encryption and auth
83+
if 'WPA3' in self.encryption:
84+
self.primary_encryption = 'WPA3'
85+
elif 'WPA2' in self.encryption:
86+
self.primary_encryption = 'WPA2'
87+
elif 'WPA' in self.encryption: # Handles cases where only "WPA" is present or if no other WPAx is found
88+
self.primary_encryption = 'WPA'
89+
elif 'WEP' in self.encryption:
90+
self.primary_encryption = 'WEP'
91+
elif 'OWE' in self.encryption: # Opportunistic Wireless Encryption
92+
self.primary_encryption = 'OWE'
93+
elif len(self.encryption) == 0: # Default to WPA if not specified, as per old logic
94+
self.primary_encryption = 'WPA'
95+
else: # Fallback for unknown types
96+
self.primary_encryption = self.encryption.split(' ')[0]
97+
98+
99+
if 'SAE' in self.authentication:
100+
self.primary_authentication = 'SAE'
101+
elif 'PSK' in self.authentication:
102+
self.primary_authentication = 'PSK'
103+
elif 'MGT' in self.authentication: # Enterprise
104+
self.primary_authentication = 'MGT'
105+
elif 'OWE' in self.authentication: # OWE uses its own auth mechanism
106+
self.primary_authentication = 'OWE'
107+
else:
108+
self.primary_authentication = self.authentication.split(' ')[0]
109+
110+
111+
self.power = int(fields[8].strip())
112+
if self.power < 0:
113+
self.power += 100
114+
self.max_power = self.power
115+
116+
self.beacons = int(fields[9].strip())
117+
self.ivs = int(fields[10].strip())
118+
119+
self.essid_known = True
120+
self.essid_len = int(fields[12].strip())
121+
self.essid = fields[13]
122+
if self.essid == '\\x00' * self.essid_len or \
123+
self.essid == 'x00' * self.essid_len or \
124+
self.essid.strip() == '':
125+
# Don't display '\x00...' for hidden ESSIDs
126+
self.essid = None # '(%s)' % self.bssid
127+
self.essid_known = False
128+
129+
# self.wps = WPSState.UNKNOWN
130+
131+
# Will be set to true once this target will be attacked
132+
# Needed to count targets in infinite attack mode
133+
self.attacked = False
134+
135+
self.decloaked = False # If ESSID was hidden but we decloaked it.
136+
137+
self.clients = []
138+
139+
# Store full encryption and authentication strings for detailed info if needed
140+
self.full_encryption_string = self.encryption
141+
self.full_authentication_string = self.authentication
142+
# For compatibility with existing logic that expects a single string:
143+
self.encryption = self.primary_encryption # Overwrite with primary for now
144+
self.authentication = self.primary_authentication # Overwrite with primary for now
145+
146+
147+
self.validate()
148+
149+
def __eq__(self, other):
150+
# Check if the other class type is either ArchivedTarget or Target
151+
return isinstance(other, (self.__class__, ArchivedTarget)) and self.bssid == other.bssid
152+
153+
def transfer_info(self, other):
154+
"""
155+
Helper function to transfer relevant fields into another Target or ArchivedTarget
156+
"""
157+
other.wps = self.wps
158+
other.attacked = self.attacked
159+
160+
if self.essid_known:
161+
if other.essid_known:
162+
other.decloaked = self.decloaked
163+
164+
if not other.essid_known:
165+
other.decloaked = self.decloaked
166+
other.essid = self.essid
167+
other.essid_known = self.essid_known
168+
other.essid_len = self.essid_len
169+
170+
# Transfer new fields as well
171+
if hasattr(self, 'primary_encryption'):
172+
other.primary_encryption = self.primary_encryption
173+
other.full_encryption_string = self.full_encryption_string
174+
if hasattr(self, 'primary_authentication'):
175+
other.primary_authentication = self.primary_authentication
176+
other.full_authentication_string = self.full_authentication_string
177+
178+
179+
def validate(self):
180+
""" Checks that the target is valid. """
181+
if self.channel == '-1':
182+
raise Exception('Ignoring target with Negative-One (-1) channel')
183+
184+
# Filter broadcast/multicast BSSIDs, see https://github.com/derv82/wifite2/issues/32
185+
bssid_broadcast = re.compile(r'^(ff:ff:ff:ff:ff:ff|00:00:00:00:00:00)$', re.IGNORECASE)
186+
if bssid_broadcast.match(self.bssid):
187+
raise Exception(f'Ignoring target with Broadcast BSSID ({self.bssid})')
188+
189+
bssid_multicast = re.compile(r'^(01:00:5e|01:80:c2|33:33)', re.IGNORECASE)
190+
if bssid_multicast.match(self.bssid):
191+
raise Exception(f'Ignoring target with Multicast BSSID ({self.bssid})')
192+
193+
def to_str(self, show_bssid=False, show_manufacturer=False):
194+
# sourcery no-metrics
195+
"""
196+
*Colored* string representation of this Target.
197+
Specifically formatted for the 'scanning' table view.
198+
"""
199+
200+
max_essid_len = 24
201+
essid = self.essid if self.essid_known else f'({self.bssid})'
202+
# Trim ESSID (router name) if needed
203+
if len(essid) > max_essid_len:
204+
essid = f'{essid[:max_essid_len - 3]}...'
205+
else:
206+
essid = essid.rjust(max_essid_len)
207+
208+
if self.essid_known:
209+
# Known ESSID
210+
essid = Color.s('{C}%s' % essid)
211+
else:
212+
# Unknown ESSID
213+
essid = Color.s('{O}%s' % essid)
214+
215+
# if self.power < self.max_power:
216+
# var = self.max_power
217+
218+
# Add a '*' if we decloaked the ESSID
219+
decloaked_char = '*' if self.decloaked else ' '
220+
essid += Color.s('{P}%s' % decloaked_char)
221+
222+
bssid = Color.s('{O}%s ' % self.bssid) if show_bssid else ''
223+
if show_manufacturer:
224+
oui = ''.join(self.bssid.split(':')[:3])
225+
self.manufacturer = Configuration.manufacturers.get(oui, "")
226+
227+
max_oui_len = 27
228+
manufacturer = Color.s('{W}%s ' % self.manufacturer)
229+
# Trim manufacturer name if needed
230+
if len(manufacturer) > max_oui_len:
231+
manufacturer = f'{manufacturer[:max_oui_len - 3]}...'
232+
else:
233+
manufacturer = manufacturer.rjust(max_oui_len)
234+
else:
235+
manufacturer = ''
236+
237+
channel_color = '{C}' if int(self.channel) > 14 else '{G}'
238+
channel = Color.s(f'{channel_color}{str(self.channel).rjust(3)}')
239+
240+
# Use primary_encryption and primary_authentication for display
241+
display_encryption = self.primary_encryption.rjust(4) # Adjusted rjust for WPA3
242+
auth_suffix = ''
243+
if self.primary_encryption == 'WPA3':
244+
display_encryption = Color.s('{P}%s' % display_encryption) # Purple for WPA3
245+
if self.primary_authentication == 'SAE':
246+
auth_suffix = Color.s('{P}-S') # Purple for SAE
247+
elif self.primary_authentication == 'MGT':
248+
auth_suffix = Color.s('{R}-E') # Red for Enterprise
249+
elif self.primary_encryption == 'WPA2':
250+
display_encryption = Color.s('{O}%s' % display_encryption) # Orange for WPA2
251+
if self.primary_authentication == 'PSK':
252+
auth_suffix = Color.s('{O}-P')
253+
elif self.primary_authentication == 'MGT':
254+
auth_suffix = Color.s('{R}-E')
255+
elif self.primary_encryption == 'WPA':
256+
display_encryption = Color.s('{O}%s' % display_encryption) # Orange for WPA
257+
if self.primary_authentication == 'PSK':
258+
auth_suffix = Color.s('{O}-P')
259+
elif self.primary_authentication == 'MGT':
260+
auth_suffix = Color.s('{R}-E')
261+
elif self.primary_encryption == 'WEP':
262+
display_encryption = Color.s('{G}%s' % display_encryption) # Green for WEP
263+
elif self.primary_encryption == 'OWE':
264+
display_encryption = Color.s('{B}%s' % display_encryption) # Blue for OWE
265+
else:
266+
display_encryption = Color.s('{W}%s' % display_encryption) # White for others
267+
268+
encryption_display_string = f"{display_encryption}{auth_suffix}".ljust(7) # Ensure consistent length
269+
270+
power = f'{str(self.power).rjust(3)}db'
271+
if self.power > 50:
272+
color = 'G'
273+
elif self.power > 35:
274+
color = 'O'
275+
else:
276+
color = 'R'
277+
power = Color.s('{%s}%s' % (color, power))
278+
279+
if self.wps == WPSState.UNLOCKED:
280+
wps = Color.s('{G} yes')
281+
elif self.wps == WPSState.NONE:
282+
wps = Color.s('{O} no')
283+
elif self.wps == WPSState.LOCKED:
284+
wps = Color.s('{R}lock')
285+
elif self.wps == WPSState.UNKNOWN:
286+
wps = Color.s('{O} n/a')
287+
else:
288+
wps = ' ERR'
289+
290+
clients = ' '
291+
if len(self.clients) > 0:
292+
clients = Color.s('{G} ' + str(len(self.clients)))
293+
294+
result = f'{essid} {bssid}{manufacturer}{channel} {encryption_display_string} {power} {wps} {clients}'
295+
296+
result += Color.s('{W}')
297+
return result
298+
299+
300+
if __name__ == '__main__':
301+
fields = 'AA:BB:CC:DD:EE:FF,2015-05-27 19:28:44,2015-05-27 19:28:46,1,54,WPA2,CCMP ' \
302+
'TKIP,PSK,-58,2,0,0.0.0.0,9,HOME-ABCD,'.split(',')
303+
t = Target(fields)
304+
t.clients.append('asdf')
305+
t.clients.append('asdf')
306+
print((t.to_str()))

wifite/util/scanner.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -148,19 +148,9 @@ def print_targets(self):
148148
Color.p('\r')
149149
return
150150

151-
if self.previous_target_count > 0 and Configuration.verbose <= 1:
152-
# Don't clear screen buffer in verbose mode.
153-
if self.previous_target_count > len(self.targets) or \
154-
Scanner.get_terminal_height() < self.previous_target_count + 3:
155-
# Either:
156-
# 1) We have less targets than before, so we can't overwrite the previous list
157-
# 2) The terminal can't display the targets without scrolling.
158-
# Clear the screen.
159-
self.clr_scr()
160-
else:
161-
# We can fit the targets in the terminal without scrolling
162-
# 'Move' cursor up, so we will print over the previous list
163-
Color.pl(Scanner.UP_CHAR * (3 + self.previous_target_count))
151+
# Always clear the screen before printing targets
152+
if Configuration.verbose <= 1:
153+
self.clr_scr()
164154

165155
self.previous_target_count = len(self.targets)
166156

0 commit comments

Comments
 (0)