1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3""" 4This is the core script for running plugins. 5 6It works by grabbing individual packets from a file or interface and feeding 7them into a chain of plugins (plugin_chain). Each plugin in the chain 8decides if the packet will continue on to the next plugin or just fade away. 9 10In practice, users generally only use one plugin, so the "chain" will only 11have one plugin, which is perfectly fine. The chain exists to allow plugins 12to alter or filter packets before passing them to more general plugins. For 13example, --plugin=country+netflow would pass packets through the country 14plugin, and then the netflow plugin. This would allow filtering traffic by 15country code before viewing flow data. 16 17Many things go into making this chain run smoothly, however. This includes 18reading in user arguments, setting filters, opening files/interfaces, etc. All 19of this largely takes place in the main() function. 20""" 21 22# standard Python library imports 23import bz2 24import faulthandler 25import gzip 26import multiprocessing 27import logging 28import operator 29import os 30import queue 31import sys 32import tempfile 33import zipfile 34from collections import OrderedDict 35from getpass import getpass 36from glob import glob 37from importlib import import_module 38from typing import Iterable 39 40import pcapy 41from pypacker.layer12 import ethernet, ppp, pppoe, ieee80211, linuxcc, radiotap, can 42from pypacker.layer3 import ip, ip6 43 44import dshell.core 45from dshell.api import get_plugin_information 46from dshell.core import Packet 47from dshell.dshelllist import get_plugins, get_output_modules 48from dshell.dshellargparse import DshellArgumentParser 49from dshell.output.output import QueueOutputWrapper 50from dshell.util import get_output_path 51from tabulate import tabulate 52 53logger = logging.getLogger(__name__) 54 55 56# plugin_chain will eventually hold the user-selected plugins that packets 57# will trickle through. 58plugin_chain = [] 59 60 61def feed_plugin_chain(plugin_index: int, packet: Packet): 62 """ 63 Every packet fed into Dshell goes through this function. 64 Its goal is to pass each packet down the chain of selected plugins. 65 Each plugin decides whether the packet(s) will proceed to the next 66 plugin, i.e. act as a filter. 67 """ 68 if plugin_index >= len(plugin_chain): 69 # We are at the end of the chain. 70 return 71 72 current_plugin = plugin_chain[plugin_index] 73 74 # Pass packet into plugin for processing. 75 current_plugin.consume_packet(packet) 76 77 # Process produced packets. 78 for _packet in current_plugin.produce_packets(): 79 feed_plugin_chain(plugin_index + 1, _packet) 80 81 82def clean_plugin_chain(plugin_index): 83 """ 84 This is called at the end of packet capture. 85 It will go through the plugins and attempt to cleanup any connections 86 that were not yet closed. 87 """ 88 if plugin_index >= len(plugin_chain): 89 # We are at the end of the chain 90 return 91 92 current_plugin = plugin_chain[plugin_index] 93 94 # need to flush even if there are no more plugins in the chain to ensure all packets are processed. 95 current_plugin.flush() 96 97 # Feed plugin chain with lingering packets released by flush. 98 for _packet in current_plugin.produce_packets(): 99 feed_plugin_chain(plugin_index + 1, _packet) 100 101 clean_plugin_chain(plugin_index + 1) 102 103 104def decompress_file(filepath, extension, unzipdir): 105 """ 106 Attempts to decompress a provided file and write the data to a temporary 107 file. The list of created temporary files is returned. 108 """ 109 filename = os.path.split(filepath)[-1] 110 openfiles = [] 111 logger.debug("Attempting to decompress {!r}".format(filepath)) 112 if extension == '.gz': 113 f = gzip.open(filepath, 'rb') 114 openfiles.append(f) 115 elif extension == '.bz2': 116 f = bz2.open(filepath, 'rb') 117 openfiles.append(f) 118 elif extension == '.zip': 119 pswd = getpass("Enter password for .zip file {!r} [default: none]: ".format(filepath)) 120 pswd = pswd.encode() # TODO I'm not sure encoding to utf-8 will work in all cases 121 try: 122 z = zipfile.ZipFile(filepath) 123 for z2 in z.namelist(): 124 f = z.open(z2, 'r', pswd) 125 openfiles.append(f) 126 except (RuntimeError, zipfile.BadZipFile) as e: 127 logger.error("Could not process .zip file {!r}. {!s}".format(filepath, e)) 128 return [] 129 130 tempfiles = [] 131 for openfile in openfiles: 132 with openfile: 133 try: 134 # check if this file is actually something decompressable 135 openfile.peek(1) 136 except OSError as e: 137 logger.error("Could not process compressed file {!r}. {!s}".format(filepath, e)) 138 continue 139 with tempfile.NamedTemporaryFile(dir=unzipdir, delete=False, prefix=filename) as tfile: 140 for piece in openfile: 141 tfile.write(piece) 142 tempfiles.append(tfile.name) 143 return tempfiles 144 145 146def print_plugins(plugins): 147 """ 148 Print list of plugins with additional info. 149 """ 150 headers = ['module', 'name', 'title', 'type', 'author', 'description'] 151 rows = [] 152 for name, module in sorted(plugins.items()): 153 rows.append([ 154 module.__module__, 155 name, 156 module.name, 157 module.__class__.__bases__[0].__name__, 158 module.author, 159 module.description, 160 ]) 161 162 print(tabulate(rows, headers=headers)) 163 164 165def main(plugin_args=None, **kwargs): 166 global plugin_chain 167 168 if not plugin_args: 169 plugin_args = {} 170 171 # dictionary of all available plugins: {name: module path} 172 plugin_map = get_plugins() 173 174 # Attempt to catch segfaults caused when certain linktypes (e.g. 204) are 175 # given to pcapy 176 faulthandler.enable() 177 178 if not plugin_chain: 179 logger.error("No plugin selected") 180 sys.exit(1) 181 182 plugin_chain[0].defrag_ip = kwargs.get("defrag", False) 183 184 # Setup logging 185 log_format = "%(levelname)s (%(name)s) - %(message)s" 186 if kwargs.get("verbose", False): 187 log_level = logging.INFO 188 elif kwargs.get("debug", False): 189 log_level = logging.DEBUG 190 elif kwargs.get("quiet", False): 191 log_level = logging.CRITICAL 192 else: 193 log_level = logging.WARNING 194 logging.basicConfig(format=log_format, level=log_level) 195 196 # since pypacker handles its own exceptions (loudly), this attempts to keep 197 # it quiet 198 logging.getLogger("pypacker").setLevel(logging.CRITICAL) 199 200 if kwargs.get("allcc", False): 201 # Activate all country code (allcc) mode to display all 3 GeoIP2 country 202 # codes 203 dshell.core.geoip.acc = True 204 205 dshell.core.geoip.check_file_dates() 206 207 # If alternate output module is selected, tell each plugin to use that 208 # instead 209 if kwargs.get("omodule", None): 210 try: 211 # TODO: Create a factory classmethod in the base Output class (e.g. "from_name()") instead. 212 omodule = import_module("dshell.output."+kwargs["omodule"]) 213 omodule = omodule.obj 214 for plugin in plugin_chain: 215 # TODO: Should we have a single instance of the Output module used by all plugins? 216 oomodule = omodule() 217 plugin.out = oomodule 218 except ImportError as e: 219 logger.error("Could not import module named '{}'. Use --list-output flag to see available modules".format(kwargs["omodule"])) 220 sys.exit(1) 221 222 # Check if any user-defined output arguments are provided 223 if kwargs.get("oargs", None): 224 oargs = {} 225 for oarg in kwargs["oargs"]: 226 if '=' in oarg: 227 key, val = oarg.split('=', 1) 228 oargs[key] = val 229 else: 230 oargs[oarg] = True 231 logger.debug("oargs: %s" % oargs) 232 for plugin in plugin_chain: 233 plugin.out.set_oargs(**oargs) 234 235 # If writing to a file, set for each output module here 236 if kwargs.get("outfile", None): 237 for plugin in plugin_chain: 238 plugin.out.reset_fh(filename=kwargs["outfile"]) 239 240 # Set nobuffer mode if that's what the user wants 241 if kwargs.get("nobuffer", False): 242 for plugin in plugin_chain: 243 plugin.out.nobuffer = True 244 245 # Set the extra flag for all output modules 246 if kwargs.get("extra", False): 247 for plugin in plugin_chain: 248 plugin.out.extra = True 249 plugin.out.set_format(plugin.out.format) 250 251 # Set the BPF filters 252 # Each plugin has its own default BPF that will be extended or replaced 253 # based on --no-vlan, --ebpf, or --bpf arguments. 254 for plugin in plugin_chain: 255 if kwargs.get("bpf", None): 256 plugin.bpf = kwargs.get("bpf", "") 257 continue 258 if plugin.bpf: 259 if kwargs.get("ebpf", None): 260 plugin.bpf = "({}) and ({})".format(plugin.bpf, kwargs.get("ebpf", "")) 261 else: 262 if kwargs.get("ebpf", None): 263 plugin.bpf = kwargs.get("ebpf", "") 264 if kwargs.get("novlan", False): 265 plugin.vlan_bpf = False 266 267 # Decide on the inputs to use for pcap 268 # If --interface is set, ignore all files and listen live on the wire 269 # Otherwise, use all of the files and globs to open offline pcap. 270 # Recurse through any directories if the command-line flag is set. 271 if kwargs.get("interface", None): 272 inputs = [kwargs.get("interface")] 273 else: 274 inputs = [] 275 inglobs = kwargs.get("files", []) 276 infiles = [] 277 for inglob in inglobs: 278 outglob = glob(inglob) 279 if not outglob: 280 logger.warning("Could not find file(s) matching {!r}".format(inglob)) 281 continue 282 infiles.extend(outglob) 283 while len(infiles) > 0: 284 infile = infiles.pop(0) 285 if kwargs.get("recursive", False) and os.path.isdir(infile): 286 morefiles = os.listdir(infile) 287 for morefile in morefiles: 288 infiles.append(os.path.join(infile, morefile)) 289 elif os.path.isfile(infile): 290 inputs.append(infile) 291 292 # Process plugin-specific options 293 for plugin in plugin_chain: 294 for option, args in plugin.optiondict.items(): 295 if option in plugin_args.get(plugin, {}): 296 setattr(plugin, option, plugin_args[plugin][option]) 297 else: 298 setattr(plugin, option, args.get("default", None)) 299 plugin.handle_plugin_options() 300 301 302 #### Dshell is ready to read pcap! #### 303 for plugin in plugin_chain: 304 plugin._premodule() 305 306 # If we are not multiprocessing, simply pass the files for processing 307 if not kwargs.get("multiprocessing", False): 308 process_files(inputs, **kwargs) 309 # If we are multiprocessing, things get more complicated. 310 else: 311 # Create an output queue, and wrap the 'write' function of each 312 # plugins's output module to send calls to the multiprocessing queue 313 output_queue = multiprocessing.Queue() 314 output_wrappers = {} 315 for plugin in plugin_chain: 316 qo = QueueOutputWrapper(plugin.out, output_queue) 317 output_wrappers[qo.id] = qo 318 plugin.out.write = qo.write 319 320 # Create processes to handle each separate input file 321 processes = [] 322 for i in inputs: 323 processes.append( 324 multiprocessing.Process(target=process_files, args=([i],), kwargs=kwargs) 325 ) 326 327 # Spawn processes, and keep track of which ones are running 328 running = [] 329 max_writes_per_batch = 50 330 while processes or running: 331 if processes and len(running) < kwargs.get("process_max", 4): 332 # Start a process and move it to the 'running' list 333 proc = processes.pop(0) 334 proc.start() 335 logger.debug("Started process {}".format(proc.pid)) 336 running.append(proc) 337 for proc in running: 338 if not proc.is_alive(): 339 # Remove finished processes from 'running' list 340 logger.debug("Ended process {} (exit code: {})".format(proc.pid, proc.exitcode)) 341 running.remove(proc) 342 try: 343 # Process write commands in the output queue. 344 # Since some plugins write copiously and may block other 345 # processes from launching, only write up to a maximum number 346 # before breaking and rechecking the processes. 347 writes = 0 348 while writes < max_writes_per_batch: 349 wrapper_id, args, kwargs = output_queue.get(True, 1) 350 owrapper = output_wrappers[wrapper_id] 351 owrapper.true_write(*args, **kwargs) 352 writes += 1 353 except queue.Empty: 354 pass 355 356 output_queue.close() 357 358 for plugin in plugin_chain: 359 plugin._postmodule() 360 361 362# Maps datalink type reported by pcapy to a pypacker packet class. 363datalink_map = { 364 1: ethernet.Ethernet, 365 9: ppp.PPP, 366 51: pppoe.PPPoE, 367 105: ieee80211.IEEE80211, 368 113: linuxcc.LinuxCC, 369 127: radiotap.Radiotap, 370 204: ppp.PPP, 371 227: can.CAN, 372 228: ip.IP, 373 229: ip6.IP6, 374} 375 376 377def read_packets(input: str, interface=False, bpf=None, count=None) -> Iterable[dshell.Packet]: 378 """ 379 Yields packets from input pcap file or device. 380 381 :param str input: device or pcap file path 382 :param bool interface: Whether input is a device. 383 :param str bpf: Optional bpf filter. 384 :param int count: Optional max count of packets to read before exiting. 385 386 :yields: packets defined by pypacker. 387 NOTE: Timestamp and frame id are added to packet for convenience. 388 """ 389 390 if interface: 391 # Listen on an interface if the option is set 392 try: 393 capture = pcapy.open_live(input, 65536, True, 1) 394 except pcapy.PcapError as e: 395 # User probably doesn't have permission to listen on interface 396 # In any case, print just the error without traceback 397 logger.error(str(e)) 398 return 399 else: 400 # Otherwise, read from pcap file(s) 401 try: 402 capture = pcapy.open_offline(input) 403 except pcapy.PcapError as e: 404 logger.error("Could not open '{}': {!s}".format(input, e)) 405 return 406 407 # TODO: We may want to allow all packets to go through and then allow the plugin to filter 408 # them out in feed_plugin_chain(). 409 # That way our frame_id won't be out of sync from skipped packets. 410 # Try and use the first plugin's BPF as the initial filter 411 # The BPFs for other plugins will be applied along the chain as needed 412 try: 413 if bpf: 414 capture.setfilter(bpf) 415 except pcapy.PcapError as e: 416 if str(e).startswith("no VLAN support for data link type"): 417 logger.error("Cannot use VLAN filters for {!r}. Recommend running with --no-vlan argument.".format(input)) 418 return 419 elif "syntax error" in str(e) or "link layer applied in wrong context" == str(e): 420 logger.error("Could not compile BPF: {!s} ({!r})".format(e, bpf)) 421 return 422 elif "802.11 link-layer types supported only on 802.11" == str(e): 423 logger.error("BPF incompatible with pcap file: {!s}".format(e)) 424 return 425 else: 426 raise e 427 428 # Set the datalink layer for each plugin, based on the pcapy capture. 429 # Also compile a pcapy BPF object for each. 430 datalink = capture.datalink() 431 for plugin in plugin_chain: 432 # TODO Find way around libpcap bug that segfaults when certain BPFs 433 # are used with certain datalink types 434 # (e.g. datalink=204, bpf="ip") 435 plugin.link_layer_type = datalink 436 plugin.recompile_bpf() 437 438 # Get correct pypacker class based on datalink layer. 439 packet_class = datalink_map.get(datalink, ethernet.Ethernet) 440 441 logger.info(f"Datalink: {datalink} - {packet_class.__name__}") 442 443 # Iterate over the file/interface and yield Packet objects. 444 frame = 1 # Start with 1 because Wireshark starts with 1. 445 while True: 446 try: 447 header, packet_data = capture.next() 448 if header is None and not packet_data: 449 # probably the end of the capture 450 break 451 if count and frame - 1 >= count: 452 # we've reached the maximum number of packets to process 453 break 454 455 # Add timestamp and frame id to packet object for convenience. 456 pktlen = header.getlen() 457 s, us = header.getts() 458 ts = s + us / 1000000.0 459 460 # Wrap packet in dshell's Packet class. 461 packet = dshell.Packet(pktlen, packet_class(packet_data), ts, frame=frame) 462 frame += 1 463 464 yield packet 465 466 except pcapy.PcapError as e: 467 estr = str(e) 468 eformat = "Error processing '{i}' - {e}" 469 if estr.startswith("truncated dump file"): 470 logger.error(eformat.format(i=input, e=estr)) 471 logger.debug(e, exc_info=True) 472 elif estr.startswith("bogus savefile header"): 473 logger.error(eformat.format(i=input, e=estr)) 474 logger.debug(e, exc_info=True) 475 else: 476 raise 477 break 478 479 480# TODO: The use of kwargs makes it difficult to understand what arguments the function accept 481# and difficult to follow the code flow. 482def process_files(inputs, **kwargs): 483 # Iterate over each of the input files 484 # For live capture, the "input" would just be the name of the interface 485 global plugin_chain 486 interface = kwargs.get("interface", False) 487 count = kwargs.get("count", None) 488 # Try and use the first plugin's BPF as the initial filter 489 # The BPFs for other plugins will be applied along the chain as needed 490 bpf = plugin_chain[0].bpf 491 492 while len(inputs) > 0: 493 input0 = inputs.pop(0) 494 495 # Check if file needs to be decompressed by its file extension 496 extension = os.path.splitext(input0)[-1] 497 if extension in (".gz", ".bz2", ".zip") and "interface" not in kwargs: 498 tempfiles = decompress_file(input0, extension, kwargs.get("unzipdir", tempfile.gettempdir())) 499 inputs = tempfiles + inputs 500 continue 501 502 for plugin in plugin_chain: 503 plugin._prefile(input0) 504 505 for packet in read_packets(input0, interface=interface, bpf=bpf, count=count): 506 feed_plugin_chain(0, packet) 507 508 clean_plugin_chain(0) 509 for plugin in plugin_chain: 510 plugin.purge() 511 plugin._postfile() 512 513 514# TODO: Separate some of this logic outside of this function so we can call 515# dshell as a library. 516def main_command_line(): 517 # Since plugin_chain contains the actual plugin instances we have to make sure 518 # we reset the global plugin_chain so multiple runs don't affect each other. 519 # (This was necessary to call this function through a python script.) 520 # TODO: Should plugin_chain be a list of plugin classes instead of instances? 521 global plugin_chain 522 plugin_chain = [] 523 524 # dictionary of all available plugins: {name: module path} 525 plugin_map = get_plugins() 526 # dictionary of plugins that the user wants to use: {name: object} 527 active_plugins = OrderedDict() 528 529 # The main argument parser. It will have every command line option 530 # available and should be used when actually parsing 531 parser = DshellArgumentParser( 532 usage="%(prog)s [options] [plugin options] file1 file2 ... fileN", 533 add_help=False) 534 parser.add_argument('-c', '--count', type=int, default=0, 535 help='Number of packets to process') 536 parser.add_argument('--debug', action="store_true", 537 help="Show debug messages") 538 parser.add_argument('-v', '--verbose', action="store_true", 539 help="Show informational messages") 540 parser.add_argument('-acc', '--allcc', action="store_true", 541 help="Show all 3 GeoIP2 country code types (represented_country/registered_country/country)") 542 parser.add_argument('-d', '-p', '--plugin', dest='plugin', type=str, 543 action='append', metavar="PLUGIN", 544 help="Use a specific plugin module. Can be chained with '+'.") 545 parser.add_argument('--defragment', dest='defrag', action='store_true', 546 help='Reconnect fragmented IP packets') 547 parser.add_argument('-h', '-?', '--help', dest='help', 548 help="Print common command-line flags and exit", action='store_true', 549 default=False) 550 parser.add_argument('-i', '--interface', default=None, type=str, 551 help="Listen live on INTERFACE instead of reading pcap") 552 parser.add_argument('-l', '--ls', '--list', action="store_true", 553 help='List all available plugins', dest='list') 554 parser.add_argument('-r', '--recursive', dest='recursive', action='store_true', 555 help='Recursively process all PCAP files under input directory') 556 parser.add_argument('--unzipdir', type=str, metavar="DIRECTORY", 557 default=tempfile.gettempdir(), 558 help='Directory to use when decompressing input files (.gz, .bz2, and .zip only)') 559 560 multiprocess_group = parser.add_argument_group("multiprocessing arguments") 561 multiprocess_group.add_argument('-P', '--parallel', dest='multiprocessing', action='store_true', 562 help='Handle each file in separate parallel processes') 563 multiprocess_group.add_argument('-n', '--nprocs', type=int, default=4, 564 metavar='NUMPROCS', dest='process_max', 565 help='Define max number of parallel processes (default: 4)') 566 567 filter_group = parser.add_argument_group("filter arguments") 568 filter_group.add_argument('--bpf', default='', type=str, 569 help="Overwrite all BPFs and use provided input. Use carefully!") 570 filter_group.add_argument('--ebpf', default='', type=str, metavar="BPF", 571 help="Extend existing BPFs with provided input for additional filtering. It will transform input into \"(<original bpf>) and (<ebpf>)\"") 572 filter_group.add_argument("--no-vlan", action="store_true", dest="novlan", 573 help="Ignore packets with VLAN headers") 574 575 output_group = parser.add_argument_group("output arguments") 576 output_group.add_argument("--lo", "--list-output", action="store_true", 577 help="List available output modules", 578 dest="listoutput") 579 output_group.add_argument("--no-buffer", action="store_true", 580 help="Do not buffer plugin output", 581 dest="nobuffer") 582 output_group.add_argument("-x", "--extra", action="store_true", 583 help="Appends extra data to all plugin output.") 584 # TODO Figure out how to make --extra flag play nicely with user-only 585 # output modules, like jsonout and csvout 586 output_group.add_argument("-O", "--omodule", type=str, dest="omodule", 587 metavar="MODULE", 588 help="Use specified output module for plugins instead of defaults. For example, --omodule=jsonout for JSON output.") 589 output_group.add_argument("--oarg", type=str, metavar="ARG=VALUE", 590 dest="oargs", action="append", 591 help="Supply a specific keyword argument to plugins' output modules. Can be used multiple times for multiple arguments. Not using an equal sign will treat it as a flag and set the value to True. Example: --oarg \"delimiter=:\" --oarg \"timeformat=%%H %%M %%S\"") 592 output_group.add_argument("-q", "--quiet", action="store_true", 593 help="Disable logging") 594 output_group.add_argument("-W", metavar="OUTFILE", dest="outfile", 595 help="Write to OUTFILE instead of stdout") 596 597 parser.add_argument('files', nargs='*', 598 help="pcap files or globs to process") 599 600 # A short argument parser, meant to only hold the simplified list of 601 # arguments for when a plugin is called without a pcap file. 602 # DO NOT USE for any serious argument parsing. 603 parser_short = DshellArgumentParser( 604 usage="%(prog)s [options] [plugin options] file1 file2 ... fileN", 605 add_help=False) 606 parser_short.add_argument('-h', '-?', '--help', dest='help', 607 help="Print common command-line flags and exit", action='store_true', 608 default=False) 609 parser.add_argument('--version', action='version', 610 version="Dshell " + str(dshell.core.__version__)) 611 parser_short.add_argument('-d', '-p', '--plugin', dest='plugin', type=str, 612 action='append', metavar="PLUGIN", 613 help="Use a specific plugin module") 614 parser_short.add_argument('--ebpf', default='', type=str, metavar="BPF", 615 help="Extend existing BPFs with provided input for additional filtering. It will transform input into \"(<original bpf>) and (<ebpf>)\"") 616 parser_short.add_argument('-i', '--interface', 617 help="Listen live on INTERFACE instead of reading pcap") 618 parser_short.add_argument('-l', '--ls', '--list', action="store_true", 619 help='List all available plugins', dest='list') 620 parser_short.add_argument("--lo", "--list-output", action="store_true", 621 help="List available output modules") 622 # FIXME: Should this duplicate option be removed? 623 parser_short.add_argument("-o", "--omodule", type=str, metavar="MODULE", 624 help="Use specified output module for plugins instead of defaults. For example, --omodule=jsonout for JSON output.") 625 parser_short.add_argument('files', nargs='*', 626 help="pcap files or globs to process") 627 628 # Start parsing the arguments 629 # Specifically, we want to grab the desired plugin list 630 # This will let us add the plugin-specific arguments and reprocess the args 631 opts, xopts = parser.parse_known_args() 632 if opts.plugin: 633 # Multiple plugins can be chained using either multiple instances 634 # of -d/-p/--plugin or joining them together with + signs. 635 plugins = '+'.join(opts.plugin) 636 plugins = plugins.split('+') 637 # check for invalid plugins 638 for plugin in plugins: 639 plugin = plugin.strip() 640 if not plugin: 641 # User probably mistyped '++' instead of '+' somewhere. 642 # Be nice and ignore this minor infraction. 643 continue 644 if plugin not in plugin_map: 645 parser_short.epilog = "ERROR! Invalid plugin provided: '{}'".format(plugin) 646 parser_short.print_help() 647 sys.exit(1) 648 # While we're at it, go ahead and import the plugin modules now 649 # This can probably be done further down the line, but here is 650 # just convenient 651 plugin_module = import_module(plugin_map[plugin]) 652 # Handle multiple instances of same plugin by appending number to 653 # end of plugin name. This is used mostly to separate 654 # plugin-specific arguments from each other 655 if plugin in active_plugins: 656 i = 1 657 plugin = plugin + str(i) 658 while plugin in active_plugins: 659 i += 1 660 plugin = plugin[:-(len(str(i-1)))] + str(i) 661 # Add copy of plugin object to chain and add to argument parsers 662 # TODO: Use class attributes for class related things like name, description, optionsdict 663 # This way we don't have to initialize the plugin at this point and fixes a lot of the 664 # issues that arise that come from dealing with a singleton. 665 active_plugins[plugin] = plugin_module.DshellPlugin() 666 plugin_chain.append(active_plugins[plugin]) 667 parser.add_plugin_arguments(plugin, active_plugins[plugin]) 668 parser_short.add_plugin_arguments(plugin, active_plugins[plugin]) 669 opts, xopts = parser.parse_known_args() 670 671 if xopts: 672 for xopt in xopts: 673 logger.warning('Could not understand argument {!r}'.format(xopt)) 674 675 if opts.help: 676 # Just print the full help message and exit 677 parser.print_help() 678 print("\n") 679 for plugin in plugin_chain: 680 print("############### {}".format(plugin.name)) 681 print(plugin.longdescription) 682 print("\n") 683 print('Default BPF: "{}"'.format(plugin.bpf)) 684 print("\n") 685 sys.exit() 686 687 if opts.list: 688 try: 689 print_plugins(get_plugin_information()) 690 except ImportError as e: 691 logger.error(e, exc_info=opts.debug) 692 sys.exit() 693 694 if opts.listoutput: 695 # List available output modules and a brief description 696 output_map = get_output_modules(get_output_path()) 697 for modulename in sorted(output_map): 698 try: 699 module = import_module("dshell.output."+modulename) 700 module = module.obj 701 except Exception as e: 702 etype = e.__class__.__name__ 703 logger.debug("Could not load {} module. ({}: {!s})".format(modulename, etype, e)) 704 else: 705 print("\t{:<25} {}".format(modulename, module._DESCRIPTION)) 706 sys.exit() 707 708 if not opts.plugin: 709 # If a plugin isn't provided, print the short help message 710 parser_short.epilog = "Select a plugin to use with -d or --plugin" 711 parser_short.print_help() 712 sys.exit() 713 714 if not opts.files and not opts.interface: 715 # If no files are provided, print the short help message 716 parser_short.epilog = "Include a pcap file to get started. Use --help for more information." 717 parser_short.print_help() 718 sys.exit() 719 720 # Process the plugin-specific args and set the attributes within them 721 plugin_args = {} 722 for plugin_name, plugin in active_plugins.items(): 723 plugin_args[plugin] = {} 724 args_and_attrs = parser.get_plugin_arguments(plugin_name, plugin) 725 for darg, dattr in args_and_attrs: 726 value = getattr(opts, darg) 727 plugin_args[plugin][dattr] = value 728 729 main(plugin_args=plugin_args, **vars(opts)) 730 731 732if __name__ == "__main__": 733 main_command_line() 734