Skip to content

Commit 0433342

Browse files
committed
Device-agnostic triggers, foxhunt mode
Added triggers that don't specify particular device ids (like any device at > -30 power). Added rough foxhunt plugin, which is a great test-drive of the plugin system.
1 parent d21722d commit 0433342

5 files changed

Lines changed: 109 additions & 27 deletions

File tree

examples/foxhunt_plugin.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Outputs the ordered list of the top 30 closest WiFi devices."""
2+
import time
3+
import heapq
4+
5+
__author__ = 'Caleb Madrigal'
6+
__email__ = '[email protected]'
7+
__version__ = '0.0.1'
8+
__apiversion__ = 1
9+
10+
TOP_N_TO_SHOW = 20
11+
12+
13+
class Trigger:
14+
def __init__(self):
15+
# Maps from dev_id to last seen signal/power level
16+
self.dev_to_power = {}
17+
18+
def __call__(self, dev_id=None, dev_type=None, power=None, **kwargs):
19+
# Only look at individual devices (device and bssids), and only look when power is present
20+
if (not power) or (not dev_id) or (dev_type == 'ssid'):
21+
return
22+
self.dev_to_power[dev_id] = power
23+
for power, dev_id in heapq.nlargest(TOP_N_TO_SHOW, [(power, mac) for mac, power in self.dev_to_power.items()]):
24+
print('{}dBm\t{}'.format(power, dev_id))
25+

trackerjacker/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
__author__ = "Caleb Madrigal"
88
__email__ = "[email protected]"
99
__license__ = "MIT"
10-
__version__ = "1.2.2"
10+
__version__ = "1.2.3"

trackerjacker/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def __init__(self,
6262
map_save_interval=10, # seconds
6363
# track args
6464
do_track=False,
65+
threshold=None,
66+
power=None,
6567
devices_to_watch=(),
6668
aps_to_watch=(),
6769
threshold_window=10,
@@ -123,6 +125,8 @@ def __init__(self,
123125
parsed_trigger_plugin = None
124126

125127
self.dot11_tracker = dot11_tracker.Dot11Tracker(self.logger,
128+
threshold,
129+
power,
126130
devices_to_watch,
127131
aps_to_watch,
128132
parsed_trigger_plugin,

trackerjacker/config_management.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,6 @@ def build_config(args):
228228
# Config from args trumps everything
229229
config.update(config_from_args)
230230

231-
# These are used to build the 'devices_to_watch' and 'aps_to_watch' below, but no longer needed after
232-
threshold = config.pop('threshold')
233-
power = config.pop('power')
234-
235231
try:
236232
config['trigger_cooldown'] = float(config['trigger_cooldown'])
237233
except ValueError:
@@ -245,18 +241,18 @@ def build_config(args):
245241

246242
# If we're in track mode and no other threshold info is set, default to a 1 byte data threshold
247243
if config['do_track']:
248-
if not threshold and not power:
249-
threshold = 1
244+
if not config['threshold'] and not config['power']:
245+
config['threshold'] = 1
250246

251247
config['devices_to_watch'] = determine_watch_list(args.devices_to_watch,
252248
devices_from_config,
253-
threshold,
254-
power)
249+
config['threshold'],
250+
config['power'])
255251

256252
config['aps_to_watch'] = determine_watch_list(args.aps_to_watch,
257253
aps_from_config,
258-
threshold,
259-
power)
254+
config['threshold'],
255+
config['power'])
260256

261257
if args.channels_to_monitor:
262258
channels_to_monitor = args.channels_to_monitor.split(',')

trackerjacker/dot11_tracker.py

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class Dot11Tracker:
2929
# pylint: disable=E1101, W0613
3030
def __init__(self,
3131
logger,
32+
threshold,
33+
power,
3234
devices_to_watch,
3335
aps_to_watch,
3436
trigger_plugin,
@@ -44,6 +46,9 @@ def __init__(self,
4446
# Same as self.arg = arg for every arg (except devices_to_watch and aps_to_watch)
4547
self.__dict__.update({k: v for k, v in locals().items() if k != 'aps_to_watch'})
4648

49+
# If no particular things are specified to be watched, assume everything should be watched
50+
self.track_all = (not aps_to_watch and not devices_to_watch)
51+
4752
# Creates a map like: {'my_ssid1': {'threshold': 5000, 'last_alert': timestamp}, 'bssid2': {...} }
4853
self.bssids_to_watch = {}
4954
self.ssids_to_watch = {}
@@ -57,32 +62,78 @@ def __init__(self,
5762
self.ssids_to_watch[ap_identifier] = watch_entry
5863

5964
def add_frame(self, frame):
60-
self.eval_device_triggers(frame.macs)
61-
self.eval_bssid_triggers(frame.bssid)
62-
self.eval_ssid_triggers(frame.ssid)
65+
if self.track_all:
66+
self.eval_general_mac_trigger(frame.macs, frame)
67+
self.eval_general_bssid_trigger(frame.bssid, frame)
68+
self.eval_general_ssid_trigger(frame.ssid, frame)
69+
else:
70+
self.eval_mac_triggers(frame.macs)
71+
self.eval_bssid_triggers(frame.bssid)
72+
self.eval_ssid_triggers(frame.ssid)
73+
74+
def eval_general_mac_trigger(self, macs, frame):
75+
for mac in macs:
76+
dev_node = self.dot11_map.get_dev_node(mac)
77+
if not dev_node:
78+
continue
79+
80+
if self.threshold:
81+
# Calculate bytes received in the alert_window
82+
bytes_in_window = (self.get_bytes_in_window(dev_node['frames_in']) +
83+
self.get_bytes_in_window(dev_node['frames_out']))
84+
if bytes_in_window >= self.threshold:
85+
self.do_trigger_alert(mac, 'mac', num_bytes=bytes_in_window)
86+
87+
if self.power and frame.signal_strength > self.power:
88+
self.do_trigger_alert(mac, 'mac', power=dev_node['signal'])
89+
90+
def eval_general_bssid_trigger(self, bssid, frame):
91+
bssid_node = self.dot11_map.get_ap_by_bssid(bssid)
92+
if self.threshold:
93+
bytes_in_window = self.get_bytes_in_window(bssid_node['frames'])
94+
if bytes_in_window >= self.threshold:
95+
self.do_trigger_alert(bssid, 'bssid', num_bytes=bytes_in_window)
6396

64-
def eval_device_triggers(self, macs):
97+
if self.power and frame.signal_strength >= self.power:
98+
self.do_trigger_alert(bssid, 'bssid', power=frame.signal_strength)
99+
100+
def eval_general_ssid_trigger(self, ssid, frame):
101+
bssid_nodes = self.dot11_map.get_ap_nodes_by_ssid(ssid)
102+
if bssid_nodes:
103+
if self.threshold:
104+
bytes_in_window = reduce(lambda acc, bssid_bytes: acc+bssid_bytes,
105+
[bssid_node['frames'] for bssid_node in bssid_nodes],
106+
0)
107+
if bytes_in_window >= self.threshold:
108+
self.do_trigger_alert(ssid, 'ssid', num_bytes=bytes_in_window)
109+
110+
if self.power and frame.signal_strength >= self.power:
111+
self.do_trigger_alert(ssid, 'bssid', power=frame.signal_strength)
112+
113+
def eval_mac_triggers(self, macs):
65114
# Only eval macs both on the "to watch" list and in the frame
66115
devices_to_eval = macs & self.devices_to_watch.keys()
67116
for mac in devices_to_eval:
68117
dev_watch_node = self.devices_to_watch[mac]
69118
dev_node = self.dot11_map.get_dev_node(mac)
70119
bytes_in_window = 0
120+
triggered = False
71121

72122
if dev_node:
73-
if dev_watch_node['power'] and dev_node['signal'] > dev_watch_node['power']:
74-
self.do_trigger_alert(mac, 'device', power=dev_node['signal'])
75-
continue
76-
elif dev_watch_node['threshold']:
123+
if dev_watch_node['threshold']:
77124
# Calculate bytes received in the alert_window
78125
bytes_in_window = (self.get_bytes_in_window(dev_node['frames_in']) +
79126
self.get_bytes_in_window(dev_node['frames_out']))
80127
if bytes_in_window >= dev_watch_node['threshold']:
81-
self.do_trigger_alert(mac, 'device', num_bytes=bytes_in_window)
82-
continue
128+
self.do_trigger_alert(mac, 'mac', num_bytes=bytes_in_window)
129+
triggered = True
130+
if dev_watch_node['power'] and dev_node['signal'] > dev_watch_node['power']:
131+
self.do_trigger_alert(mac, 'mac', power=dev_node['signal'])
132+
triggered = True
83133

84-
self.logger.debug('Bytes received for {} (threshold: {}) in last {} seconds: {}'
85-
.format(mac, dev_watch_node['threshold'], self.threshold_window, bytes_in_window))
134+
if not triggered:
135+
self.logger.debug('Bytes received for {} (threshold: {}) in last {} seconds: {}'
136+
.format(mac, dev_watch_node['threshold'], self.threshold_window, bytes_in_window))
86137

87138
def eval_bssid_triggers(self, bssid):
88139
if bssid not in self.bssids_to_watch:
@@ -91,15 +142,21 @@ def eval_bssid_triggers(self, bssid):
91142
bssid_watch_node = self.bssids_to_watch[bssid]
92143
bssid_node = self.dot11_map.get_ap_by_bssid(bssid)
93144
bytes_in_window = 0
145+
triggered = False
94146

95147
if bssid_node:
148+
if bssid_watch_node['power'] and bssid_node['signal'] >= bssid_watch_node['power']:
149+
self.do_trigger_alert(bssid, 'bssid', power=bssid_node['signal'])
150+
triggered = True
151+
96152
bytes_in_window = self.get_bytes_in_window(bssid_node['frames'])
97-
if bytes_in_window >= bssid_watch_node['threshold']:
153+
if bssid_watch_node['threshold'] and bytes_in_window >= bssid_watch_node['threshold']:
98154
self.do_trigger_alert(bssid, 'bssid', num_bytes=bytes_in_window)
99-
return
155+
triggered = True
100156

101-
self.logger.info('Bytes received for {} in last {} seconds: {}'
102-
.format(bssid, self.threshold_window, bytes_in_window))
157+
if not triggered:
158+
self.logger.info('Bytes received for {} in last {} seconds: {}'
159+
.format(bssid, self.threshold_window, bytes_in_window))
103160

104161
def eval_ssid_triggers(self, ssid):
105162
if ssid not in self.ssids_to_watch:

0 commit comments

Comments
 (0)