trackerjacker 2.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ """
2
+ trackerjacker
3
+
4
+ Finds and tracks wifi devices through raw 802.11 monitoring
5
+ """
6
+
7
+ __author__ = "Caleb Madrigal"
8
+ __email__ = "caleb.madrigal@gmail.com"
9
+ __license__ = "MIT"
10
+
11
+ from .version import __version__
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env python3
2
+ # pylint: disable=C0111, C0103, W0703, R0902, R0903, R0912, R0913, R0914, R0915, C0413
3
+
4
+ import os
5
+ import sys
6
+ import time
7
+ import json
8
+ import errno
9
+ import pprint
10
+ import logging
11
+ import platform
12
+ import traceback
13
+
14
+ logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
15
+ import scapy.all as scapy
16
+
17
+ from . import config_management
18
+ from . import dot11_frame
19
+ from . import dot11_mapper
20
+ from . import dot11_tracker
21
+ from . import plugin_parser
22
+ from . import ieee_mac_vendor_db
23
+ from .common import TJException
24
+
25
+ if platform.system() == 'Linux':
26
+ from . import linux_device_management as device_management
27
+ elif platform.system() == 'Darwin':
28
+ from . import macos_device_management as device_management
29
+
30
+ LOG_NAME_TO_LEVEL = {'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'ERROR': 40, 'CRITICAL': 50}
31
+
32
+
33
+ def make_logger(log_path=None, log_level_str='INFO'):
34
+ logger = logging.getLogger('trackerjacker')
35
+ formatter = logging.Formatter('%(asctime)s: (%(levelname)s): %(message)s')
36
+
37
+ if log_path:
38
+ log_handler = logging.FileHandler(log_path)
39
+ log_handler.setFormatter(formatter)
40
+ logger.addHandler(log_handler)
41
+
42
+ stderr_handler = logging.StreamHandler(sys.stderr)
43
+ stderr_handler.setLevel(logging.ERROR)
44
+ stderr_handler.setFormatter(logging.Formatter('%(message)s'))
45
+ logger.addHandler(stderr_handler)
46
+ else:
47
+ log_handler = logging.StreamHandler(sys.stdout)
48
+ log_handler.setFormatter(logging.Formatter('%(message)s'))
49
+ logger.addHandler(log_handler)
50
+
51
+ log_level = LOG_NAME_TO_LEVEL.get(log_level_str.upper(), 20)
52
+ logger.setLevel(log_level)
53
+ return logger
54
+
55
+
56
+ class TrackerJacker:
57
+ def __init__(self,
58
+ logger=None,
59
+ iface=None,
60
+ channels_to_monitor=None,
61
+ channel_switch_scheme='default',
62
+ time_per_channel=2,
63
+ display_matching_packets=False,
64
+ display_all_packets=False,
65
+ # map args
66
+ do_map=True,
67
+ map_file='wifi_map.yaml',
68
+ map_save_interval=10, # seconds
69
+ # track args
70
+ do_track=False,
71
+ threshold=None,
72
+ power=None,
73
+ devices_to_watch=(),
74
+ aps_to_watch=(),
75
+ threshold_window=10,
76
+ trigger_plugin=None,
77
+ plugin_config=None,
78
+ trigger_command=None,
79
+ trigger_cooldown=30, # seconds
80
+ beep_on_trigger=False,
81
+ remove_unseen_devices=False):
82
+
83
+ self.iface = iface
84
+ self.do_map = do_map
85
+ self.do_track = do_track
86
+ self.map_file = map_file
87
+ self.map_save_interval = map_save_interval
88
+ self.display_matching_packets = display_matching_packets
89
+ self.display_all_packets = display_all_packets
90
+ self.mac_vendor_db = ieee_mac_vendor_db.MacVendorDB()
91
+
92
+ if logger:
93
+ self.logger = logger
94
+ else:
95
+ self.logger = make_logger()
96
+
97
+ # Even if we are not in map mode, we still need to build the map for tracking purposes
98
+ self.dot11_map = None
99
+ if self.do_map:
100
+ self.map_last_save = time.time()
101
+
102
+ # Try to load map
103
+ self.logger.info('Map output file: %s', self.map_file)
104
+ if os.path.exists(self.map_file):
105
+ self.dot11_map = dot11_mapper.Dot11Map.load_from_file(self.map_file, remove_unseen_devices)
106
+ if self.dot11_map:
107
+ self.logger.info('Loaded %d devices and %d ssids from %s',
108
+ len(self.dot11_map.devices),
109
+ len(self.dot11_map.ssid_to_access_point),
110
+ self.map_file)
111
+ else:
112
+ self.logger.warning('Specified map file not found - creating new map file.')
113
+
114
+ if not self.dot11_map:
115
+ self.dot11_map = dot11_mapper.Dot11Map(remove_unseen_devices=remove_unseen_devices)
116
+
117
+ self.dot11_map.window = threshold_window
118
+
119
+ if channel_switch_scheme == 'default':
120
+ if self.do_map:
121
+ channel_switch_scheme = 'round_robin'
122
+ else: # track mode
123
+ channel_switch_scheme = 'traffic_based'
124
+
125
+ print(f'devices_to_watch = {devices_to_watch}, aps_to_watch = {aps_to_watch}')
126
+ self.devices_to_watch_set = set([dev['mac'].lower() for dev in devices_to_watch if 'mac' in dev])
127
+ self.aps_to_watch_set = set([ap['bssid'].lower() for ap in aps_to_watch if 'bssid' in ap])
128
+
129
+ if len(self.devices_to_watch_set) > 0:
130
+ print(f'Tracking devices: {self.devices_to_watch_set}')
131
+ if len(self.aps_to_watch_set) > 0:
132
+ print(f'Tracking Access Points: {self.aps_to_watch_set}')
133
+
134
+ if self.do_track:
135
+ # Build trigger hit function
136
+ if trigger_plugin:
137
+ trigger_plugin = config_management.get_real_plugin_path(trigger_plugin)
138
+ parsed_trigger_plugin = plugin_parser.parse_trigger_plugin(trigger_plugin, plugin_config)
139
+ else:
140
+ parsed_trigger_plugin = None
141
+
142
+ self.dot11_tracker = dot11_tracker.Dot11Tracker(self.logger,
143
+ threshold,
144
+ power,
145
+ devices_to_watch,
146
+ aps_to_watch,
147
+ parsed_trigger_plugin,
148
+ trigger_command,
149
+ trigger_cooldown,
150
+ threshold_window,
151
+ beep_on_trigger,
152
+ self.dot11_map)
153
+
154
+ self.iface_manager = device_management.Dot11InterfaceManager(iface,
155
+ self.logger,
156
+ channels_to_monitor,
157
+ channel_switch_scheme,
158
+ time_per_channel)
159
+
160
+ def process_packet(self, pkt):
161
+ try:
162
+ if pkt.haslayer(scapy.Dot11):
163
+ looking_for_specifics_and_none_found = self.aps_to_watch_set or self.devices_to_watch_set
164
+
165
+ try:
166
+ frame = dot11_frame.Dot11Frame(pkt,
167
+ int(self.iface_manager.current_channel),
168
+ iface=self.iface_manager.iface)
169
+ except Exception as e:
170
+ # Thank you DEF CON (https://github.com/secdev/scapy/issues/1552)
171
+ self.logger.warning('Error decoding Dot11Frame: %s', e)
172
+ return
173
+
174
+ if self.do_map:
175
+ self.log_newly_found(frame)
176
+
177
+ if self.display_all_packets:
178
+ print('\t', pkt.summary())
179
+
180
+ # See if any APs we care about (if we're looking for specific APs)
181
+ if self.aps_to_watch_set:
182
+ if frame.bssid not in self.aps_to_watch_set:
183
+ looking_for_specifics_and_none_found = False
184
+
185
+ # See if any MACs we care about (if we're looking for specific MACs)
186
+ if self.devices_to_watch_set:
187
+ matched_macs = self.devices_to_watch_set & frame.macs
188
+ if matched_macs:
189
+ looking_for_specifics_and_none_found = False
190
+
191
+ # Display matched packets (if specified)
192
+ if self.display_matching_packets and not self.display_all_packets:
193
+ print('\t', pkt.summary())
194
+
195
+ # If we are looking for specific APs or Devices and none are found, no further processing needed
196
+ if looking_for_specifics_and_none_found:
197
+ return
198
+
199
+ # If map mode enabled, do it. Note that we don't exclude non-matching MACs from the mapping
200
+ # (which is why this isn't under the 'if matched_matcs' block).
201
+ # Note: we update the map whether do_map is true or false since it's used for tracking; just don't save map
202
+ self.dot11_map.add_frame(frame)
203
+ if self.do_map:
204
+ if time.time() - self.map_last_save >= self.map_save_interval:
205
+ self.dot11_map.save_to_file(self.map_file)
206
+ self.map_last_save = time.time()
207
+
208
+ if self.do_track:
209
+ self.dot11_tracker.add_frame(frame, pkt)
210
+
211
+ # Update device tracking (for traffic-based)
212
+ self.iface_manager.add_frame(frame)
213
+ except Exception as e:
214
+ self.logger.debug('Error in process_packet: {e}')
215
+
216
+ def log_newly_found(self, frame):
217
+ # Log newly-found things
218
+ if frame.ssid and frame.bssid not in self.dot11_map.access_points.keys():
219
+ self.logger.info('SSID found: %s, BSSID: %s, Channel: %d', frame.ssid, frame.bssid, frame.channel)
220
+
221
+ new_macs = [mac for mac in frame.macs
222
+ if mac not in (self.dot11_map.devices.keys() |
223
+ self.dot11_map.access_points.keys() |
224
+ dot11_mapper.MACS_TO_IGNORE)]
225
+ for mac in new_macs:
226
+ if mac: # The frame can be crafted to include a null mac
227
+ self.logger.info('MAC found: %s, Channel: %d', mac, frame.channel)
228
+
229
+ def start(self):
230
+ self.logger.debug('Starting monitoring on %s', self.iface_manager.iface)
231
+ self.iface_manager.start()
232
+ while True:
233
+ try:
234
+ # macOS
235
+ if platform.system() == 'Darwin':
236
+ self.logger.warning('macOS support is pre-alpha - many improvements coming soon')
237
+ scapy.sniff(iface=self.iface_manager.iface, monitor=True, prn=self.process_packet, store=0, count=0)
238
+ break
239
+ # linux
240
+ else:
241
+ # For versions of scapy that don't provide the exceptions kwarg
242
+ scapy.sniff(iface=self.iface_manager.iface, prn=self.process_packet, store=0, count=0)
243
+ break
244
+
245
+ except TJException:
246
+ raise
247
+ except (OSError, IOError):
248
+ self.logger.error(traceback.format_exc())
249
+ self.logger.info('Sniffer error occurred. Restarting sniffer in 3 seconds...')
250
+ time.sleep(3)
251
+
252
+ def stop(self):
253
+ self.iface_manager.stop()
254
+
255
+ if self.do_map:
256
+ # Flush map to disk
257
+ self.dot11_map.save_to_file(self.map_file)
258
+
259
+
260
+ def do_simple_tasks_if_specified(args):
261
+ if args.version:
262
+ from .version import __version__
263
+ print('trackerjacker {}'.format(__version__))
264
+ sys.exit(0)
265
+ elif args.do_enable_monitor_mode:
266
+ if not args.iface:
267
+ raise TJException('You must specify the interface with the -i paramter')
268
+ device_management.monitor_mode_on(args.iface)
269
+ print('Enabled monitor mode on {}'.format(args.iface))
270
+ sys.exit(0)
271
+ elif args.do_disable_monitor_mode:
272
+ if not args.iface:
273
+ raise TJException('You must specify the interface with the -i paramter')
274
+ device_management.monitor_mode_off(args.iface)
275
+ print('Disabled monitor mode on {}'.format(args.iface))
276
+ sys.exit(0)
277
+ elif args.mac_lookup:
278
+ vendor = ieee_mac_vendor_db.MacVendorDB().lookup(args.mac_lookup)
279
+ if vendor:
280
+ print(vendor)
281
+ else:
282
+ print('Vendor for {} not found'.format(args.mac_lookup), file=sys.stderr)
283
+ sys.exit(0)
284
+ elif args.print_default_config:
285
+ print(json.dumps(config_management.DEFAULT_CONFIG, indent=4, sort_keys=True))
286
+ sys.exit(0)
287
+ elif args.set_channel:
288
+ if not args.iface:
289
+ raise TJException('You must specify the interface with the -i paramter')
290
+ channel = args.set_channel[0]
291
+ device_management.switch_to_channel(args.iface, channel)
292
+ print('Set channel to {} on {}'.format(channel, args.iface))
293
+ sys.exit(0)
294
+
295
+
296
+ def main():
297
+ argparse_args = config_management.get_arg_parser().parse_args()
298
+
299
+ # Some command-line args specify to just perform a simple task and then exit
300
+ try:
301
+ do_simple_tasks_if_specified(argparse_args)
302
+ except TJException as e:
303
+ print('Error: {}'.format(e), file=sys.stderr)
304
+ sys.exit(1)
305
+
306
+ try:
307
+ config = config_management.build_config(argparse_args)
308
+ except TJException as e:
309
+ print('{}'.format(e))
310
+ sys.exit(1)
311
+
312
+ if config['log_level'] == 'DEBUG':
313
+ print('Config:')
314
+ pprint.pprint(config)
315
+
316
+ # Setup logger
317
+ logger = make_logger(config.pop('log_path'), config.pop('log_level'))
318
+
319
+ # Any actual trackerjacker usage requires root (monitor mode in scapy requires it)
320
+ if not os.getuid() == 0:
321
+ print('trackerjacker requires r00t!', file=sys.stderr)
322
+ sys.exit(errno.EPERM)
323
+
324
+ try:
325
+ tj = TrackerJacker(**dict(config, **{'logger': logger})) # pylint: disable=E1123
326
+ tj.start()
327
+ except TJException as e:
328
+ logger.critical('Error: %s', e)
329
+ except KeyboardInterrupt:
330
+ print('Stopping...')
331
+ finally:
332
+ try:
333
+ tj.stop()
334
+ except UnboundLocalError:
335
+ # Exception was thrown in TrackerJacker initializer, so 'tj' doesn't exist
336
+ pass
337
+
338
+ if __name__ == '__main__':
339
+ main()
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0111
2
+ MACS_TO_IGNORE = {'ff:ff:ff:ff:ff:ff', '00:00:00:00:00:00'}
3
+
4
+
5
+ class TJException(Exception):
6
+ pass
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env python3
2
+ # pylint: disable=C0111, C0103, W0703, R0902, R0903, R0912, R0913, R0914, R0915, C0413
3
+
4
+ import os
5
+ import copy
6
+ import json
7
+ import argparse
8
+ from .common import TJException
9
+ from . import plugin_parser
10
+
11
+ # Default config
12
+ DEFAULT_CONFIG = {'log_path': None,
13
+ 'log_level': 'INFO',
14
+ 'iface': None,
15
+ 'devices_to_watch': [],
16
+ 'aps_to_watch': [],
17
+ 'threshold': None,
18
+ 'power': None,
19
+ 'threshold_window': 10,
20
+ 'do_map': True,
21
+ 'do_track': False,
22
+ 'map_file': 'wifi_map.yaml',
23
+ 'map_save_interval': 10,
24
+ 'trigger_plugin': None,
25
+ 'plugin_config': None,
26
+ 'trigger_command': None,
27
+ 'trigger_cooldown': 30,
28
+ 'channels_to_monitor': None,
29
+ 'channel_switch_scheme': 'default',
30
+ 'time_per_channel': 0.5,
31
+ 'display_matching_packets': False,
32
+ 'display_all_packets': False,
33
+ 'beep_on_trigger': False,
34
+ 'remove_unseen_devices': False}
35
+
36
+
37
+ def get_arg_parser():
38
+ """Returns the configured argparse object."""
39
+ parser = argparse.ArgumentParser()
40
+ # Modes
41
+ parser.add_argument('--map', action='store_true', dest='do_map',
42
+ help='Map mode - output map to wifi_map.yaml')
43
+ parser.add_argument('--track', action='store_true', dest='do_track',
44
+ help='Track mode')
45
+ parser.add_argument('--remove-unseen-devices', action='store_true', dest='remove_unseen_devices',
46
+ help='If enabled, map will forget about devices not seen for over five minutes')
47
+ parser.add_argument('--monitor-mode-on', action='store_true', dest='do_enable_monitor_mode',
48
+ help='Enables monitor mode on the specified interface and exit')
49
+ parser.add_argument('--monitor-mode-off', action='store_true', dest='do_disable_monitor_mode',
50
+ help='Disables monitor mode on the specified interface and exit')
51
+ parser.add_argument('--set-channel', metavar='CHANNEL', dest='set_channel', nargs=1,
52
+ help='Set the specified wireless interface to the specified channel and exit')
53
+ parser.add_argument('--mac-lookup', type=str, dest='mac_lookup',
54
+ help='Lookup the vendor of the specified MAC address and exit')
55
+ parser.add_argument('--print-default-config', action='store_true', dest='print_default_config',
56
+ help='Print boilerplate config file and exit')
57
+
58
+ # Normal switches
59
+ parser.add_argument('-v', '--version', action='store_true', dest='version',
60
+ help='Display trackerjacker version')
61
+ parser.add_argument('-i', '--interface', type=str, dest='iface',
62
+ help='Network interface to use; if empty, try to find monitor inferface')
63
+ parser.add_argument('-m', '--macs', type=str, dest='devices_to_watch',
64
+ help='MAC(s) to track; comma separated for multiple')
65
+ parser.add_argument('-a', '--access-points', type=str, dest='aps_to_watch',
66
+ help='Access point(s) to track - specified by BSSID; comma separated for multiple')
67
+ parser.add_argument('--channels-to-monitor', type=str, dest='channels_to_monitor',
68
+ help='Channels to monitor; comma separated for multiple')
69
+ parser.add_argument('--channel-switch-scheme', type=str, dest='channel_switch_scheme',
70
+ help='Options: "round_robin" or "traffic_based"', default='default')
71
+ parser.add_argument('--time-per-channel', type=float, dest='time_per_channel',
72
+ help='Seconds spent on each channel before hopping')
73
+ parser.add_argument('-w', '--time-window', type=int, dest='threshold_window',
74
+ help='Time window (in seconds) which alert threshold is applied to')
75
+ parser.add_argument('--map-save-interval', type=float, dest='map_save_interval',
76
+ help='Number of seconds between saving the wifi map to disk')
77
+ parser.add_argument('--threshold', type=int, dest='threshold',
78
+ help='Default data threshold (unless overridden on a per-dev basis) for triggering')
79
+ parser.add_argument('--power', type=int, dest='power',
80
+ help='Default power threshold (unless overridden on a per-dev basis) for triggering')
81
+ parser.add_argument('--plugin', type=str, dest='trigger_plugin',
82
+ help='Python trigger plugin file path; for more information')
83
+ parser.add_argument('--trigger-plugin', type=str, dest='trigger_plugin',
84
+ help='Python trigger plugin file path; for more information')
85
+ parser.add_argument('--plugin-config', type=str, dest='plugin_config',
86
+ help='Config to pass to python trigger plugin. Must be a python dict or json obj.')
87
+ parser.add_argument('--trigger-command', type=str, dest='trigger_command',
88
+ help='Command to execute upon alert')
89
+ parser.add_argument('--trigger-cooldown', type=str, dest='trigger_cooldown',
90
+ help='Time in seconds between trigger executions for a particular device')
91
+ parser.add_argument('--display-all-packets', action='store_true', dest='display_all_packets',
92
+ help='If true, displays all packets matching filters')
93
+ parser.add_argument('--beep-on-trigger', action='store_true', dest='beep_on_trigger',
94
+ help='If enabled, beep each time a trigger hits (off by default)')
95
+ parser.add_argument('--map-file', type=str, dest='map_file', default='wifi_map.yaml',
96
+ help='File path to which to output wifi map; default: wifi_map.yaml')
97
+ parser.add_argument('--log-path', type=str, dest='log_path', default=None,
98
+ help='Log path; default is stdout')
99
+ parser.add_argument('--log-level', type=str, dest='log_level', default='INFO',
100
+ help='Log level; Options: DEBUG, INFO, WARNING, ERROR, CRITICAL')
101
+ parser.add_argument('-c', '--config', type=str, dest='config',
102
+ help='Path to config json file; For example config file, use --print-default-config')
103
+ return parser
104
+
105
+
106
+ def parse_command_line_watch_list(watch_str):
107
+ """Parse string that represents devices to watch.
108
+
109
+ Valid examples:
110
+ * aa:bb:cc:dd:ee:ff
111
+ - Threshold of 1 for the given MAC address
112
+ * aa:bb:cc:dd:ee:ff,11:22:33:44:55:66
113
+ - This means look for any traffic from either address
114
+ * aa:bb:cc:dd:ee:ff=1337, 11:22:33:44:55:66=1000
115
+ - This means look for 1337 bytes for the first address, and 1000 for the second
116
+ * my_ssid, 11:22:33:44:55:66=1000
117
+ - This means look for 1 byte from my_ssid or 1000 for the second
118
+ * 11:22:33:44:55:66=-30
119
+ - This means trigger if 11:22:33:44:55:66 is seen at a power level >= -30dBm (negative value implies power)
120
+
121
+ Returns dict in this format:
122
+ {'aa:bb:cc:dd:ee:ff': {'threshold': 100, 'power': None},
123
+ '11:22:33:44:55:66': {'threshold': None, 'power': -30}}
124
+ """
125
+
126
+ watch_list = [i.strip() for i in watch_str.split(',')]
127
+ watch_dict = {}
128
+
129
+ for watch_part in watch_list:
130
+ power = None
131
+ threshold = None
132
+
133
+ if '=' in watch_part:
134
+ # dev_id is a MAC, BSSID, or SSID
135
+ dev_id, val = [i.strip() for i in watch_part.split('=')]
136
+ try:
137
+ val = int(val)
138
+ except ValueError:
139
+ # Can't parse with "dev_id=threshold" formula, so assume '=' sign was part of ssid
140
+ dev_id = watch_part
141
+
142
+ if val > 0:
143
+ threshold = val
144
+ else:
145
+ power = val
146
+ else:
147
+ dev_id = watch_part
148
+
149
+ watch_dict[dev_id] = {'threshold': threshold, 'power': power}
150
+
151
+ return watch_dict
152
+
153
+
154
+ def determine_watch_list(to_watch_from_args,
155
+ to_watch_from_config,
156
+ generic_threshold,
157
+ generic_power):
158
+ """Builds the list of devices to watch.
159
+
160
+ Coalesces the to_watch list from the command-line arguments, the config file,
161
+ and the the general threshold, power, and trigger command. The main idea here is to look
162
+ for the config values set on a per-device basis, and prioritize those, but if they are not there,
163
+ fall back to the "generic_*" version. And if those have not been specified, fall back to defaults.
164
+
165
+ Example input:
166
+ to_watch_from_args = 'aa:bb:cc:dd:ee:ff=1337, 11:22:33:44:55:66=100, ff:ee:dd:cc:bb:aa',
167
+ to_watch_from_config = {}
168
+ generic_threshold = 1337
169
+ generic_power = None
170
+
171
+ Returns a dict in this format:
172
+ {
173
+ 'aa:bb:cc:dd:ee:ff': {'threshold': None, 'power': -40},
174
+ '11:22:33:44:55:66': {'threshold': 100, 'power': None},
175
+ 'ff:ee:dd:cc:bb:aa': {'threshold': 1337, 'power': None}
176
+ }
177
+ """
178
+
179
+ # Converts from cli param format like: "aa:bb:cc:dd:ee:ff=-40,11:22:33:44:55:66=100' to a map like:
180
+ # {'aa:bb:cc:dd:ee:ff': {'threshold': None, 'power': -40},
181
+ # '11:22:33:44:55:66': {'threshold': 100, 'power': None}
182
+ if to_watch_from_args:
183
+ to_watch_from_args = parse_command_line_watch_list(to_watch_from_args)
184
+
185
+ if not to_watch_from_args:
186
+ to_watch_from_args = {}
187
+ if not to_watch_from_config:
188
+ to_watch_from_config = {}
189
+
190
+ watch_config_dict = {}
191
+ for dev_id in to_watch_from_args.keys() | to_watch_from_config.keys():
192
+ watch_entry = {'threshold': None,
193
+ 'power': None}
194
+ watch_entry['threshold'] = (to_watch_from_args.get(dev_id, {}).get('threshold', None) or
195
+ to_watch_from_config.get(dev_id, {}).get('threshold', None))
196
+ watch_entry['power'] = (to_watch_from_args.get(dev_id, {}).get('power', None) or
197
+ to_watch_from_config.get(dev_id, {}).get('power', None))
198
+
199
+ if not watch_entry['threshold'] and not watch_entry['power']:
200
+ if generic_threshold and not generic_power:
201
+ watch_entry['threshold'] = generic_threshold
202
+ elif generic_power:
203
+ watch_entry['power'] = generic_power
204
+ else:
205
+ watch_entry['threshold'] = 1
206
+
207
+ watch_config_dict[dev_id] = watch_entry
208
+
209
+ return watch_config_dict
210
+
211
+
212
+ def build_config(args):
213
+ """Builds the config from the command-line args, config file, and defaults."""
214
+ config = copy.deepcopy(DEFAULT_CONFIG)
215
+ devices_from_config = {}
216
+ aps_from_config = {}
217
+
218
+ if args.config:
219
+ try:
220
+ with open(args.config, 'r') as f:
221
+ config_from_file = json.loads(f.read())
222
+
223
+ # If there are any keys defined in the config file not allowed, error out
224
+ invalid_keys = set(config_from_file.keys()) - set(config.keys())
225
+ if invalid_keys:
226
+ raise TJException('Invalid keys found in config file: {}'.format(invalid_keys))
227
+
228
+ devices_from_config = config_from_file.pop('devices_to_watch', {})
229
+ aps_from_config = config_from_file.pop('aps_to_watch', {})
230
+
231
+ config.update(config_from_file)
232
+ print('Loaded configuration from {}'.format(args.config))
233
+
234
+ except (IOError, OSError, json.decoder.JSONDecodeError) as e:
235
+ raise TJException('Error loading config file ({}): {}'.format(args.config, e))
236
+
237
+ non_config_args = {'config', 'devices_to_watch', 'aps_to_watch', 'do_enable_monitor_mode', 'version',
238
+ 'do_disable_monitor_mode', 'set_channel', 'print_default_config', 'mac_lookup'}
239
+
240
+ config_from_args = vars(args)
241
+ config_from_args = {k: v for k, v in config_from_args.items()
242
+ if v is not None and k not in non_config_args}
243
+
244
+ # Config from args trumps default or config file
245
+ config.update(config_from_args)
246
+
247
+ # Allow any plugins to override config
248
+ if config['trigger_plugin']:
249
+ trigger_plugin_path = get_real_plugin_path(config['trigger_plugin'])
250
+ parsed_trigger_plugin = plugin_parser.parse_trigger_plugin(trigger_plugin_path,
251
+ config['plugin_config'],
252
+ parse_only=True)
253
+
254
+ # Allow plugin to override any config parameters
255
+ if 'config' in parsed_trigger_plugin:
256
+ trigger_config = parsed_trigger_plugin['config']
257
+ config.update(**trigger_config)
258
+
259
+ try:
260
+ config['trigger_cooldown'] = float(config['trigger_cooldown'])
261
+ except ValueError:
262
+ raise TJException('trigger_cooldown must be a number')
263
+
264
+ # If we're in track mode and no other threshold info is set, default to a 1 byte data threshold
265
+ if config['do_track']:
266
+ if not config['threshold'] and not config['power']:
267
+ config['threshold'] = 1
268
+
269
+ config['devices_to_watch'] = determine_watch_list(args.devices_to_watch,
270
+ devices_from_config,
271
+ config['threshold'],
272
+ config['power'])
273
+
274
+ config['aps_to_watch'] = determine_watch_list(args.aps_to_watch,
275
+ aps_from_config,
276
+ config['threshold'],
277
+ config['power'])
278
+
279
+ if args.channels_to_monitor:
280
+ channels_to_monitor = args.channels_to_monitor.split(',')
281
+ config['channels_to_monitor'] = channels_to_monitor
282
+
283
+ return config
284
+
285
+
286
+ def get_real_plugin_path(trigger_plugin):
287
+ if not trigger_plugin.lower().endswith('.py') and '/' not in trigger_plugin:
288
+ possible_builtin_path = os.path.join(os.path.dirname(__file__),
289
+ 'plugins',
290
+ '{}.py'.format(trigger_plugin))
291
+ if os.path.exists(possible_builtin_path):
292
+ trigger_plugin = possible_builtin_path
293
+ return trigger_plugin