'''

This is the conductor class which essentially controls everything for this
tool.

'''


import csv
import glob
import imp
import netaddr
import os
import pickle
import sys
import time
import colorama
from common import helpers
from common import ip_object
from modules.analytics import *
from modules.intelgathering import *


class Conductor:

    def __init__(self, command_args):
        # Create dictionaries of supported modules
        # empty until stuff loaded into them
        # IP Object module will be where all IPs and their objects (with info)
        # about them will be stored
        # Stored in following format {'IPAddress': [IP_Object, # Instances of IP]}
        self.system_objects = {}

        # Intel gathering transforms is used to gather information about the
        # loaded IPs
        self.intelgathering_transforms = {}

        # Analytical transforms perform actions against the IPs loaded into
        # the framework
        self.analytical_transforms = {}

        # Fix the unicode error issue
        reload(sys)
        sys.setdefaultencoding('utf-8')

        # Command line arguments
        self.cli_args = command_args

        # help_commands just contains commands to be used in the "shell"
        self.commands = {
            "analyze": "Run [module] on the loaded IP addresses",
            "export" : "Exports all data on all IPs to CSV (opt: filename)",
            "gather" : "Requests information and gathers statistics on loaded IP addresses",
            "help   ": "Displays commands and command descriptions",
            "import ": "Import's saved state into Just Metadata",
            "ip_info": "Display's all info about an IP address",
            "load   ": "Loads IPs into the framework for analysis",
            "list   ": "Prints loaded [analysis] or [gather] modules",
            "save   ": "Saves IPs and attributes to disk for reloading in the future (opt: filename)",
            "exit   ": "Exits out of Just-Metadata"
        }

        # command given by the user
        self.user_command = ""

        # Load the intel gathering modules
        self.load_intelgathering_functions()

        # Load the analytical modules
        self.load_analytical_functions()

    def check_cli(self):
        if self.cli_args is not None:
            sys.exit()
        return

    # This collapse function came from @harmj0y, thanks for the help with
    # it man
    def collapse(self, var, tabs=0):
        result = ""

        if type(var) is dict:
            for field, value in var.iteritems():
                try:
                    result += "\n" + tabs * "\t" + field.encode('utf-8') + ": " + self.collapse(
                        value, tabs=(tabs + 1))
                except UnicodeDecodeError:
                    result += "\n" + tabs * "\t" + field.encode('utf-8') + ": " + self.collapse(
                        value.encode('utf-8'), tabs=(tabs + 1))

        elif type(var) is list:
            for l in var:
                result += self.collapse(l, tabs=tabs) + "\n"

        elif var is None:
            result += "No Information Available"

        elif type(var) is float or type(var) is int or type(var) is long\
                or type(var) is bool:
            result += str(var)

        else:
            result += str(var.encode('utf-8'))
        return result

    def export_info(self, f):
        # Date and Time for export File
        current_date = time.strftime("%m/%d/%Y").replace("/", "")
        current_time = time.strftime("%H:%M:%S").replace(":", "")

        if f == "":
            f = 'export_' + current_date + '_' + current_time + '.csv'      

        # True for printing the header on the first system
        # after that, only values
        add_header = True

        for path, ip_objd in self.system_objects.iteritems():
            attrs = vars(ip_objd[0])
            try:
                with open(f, 'a') as export_file:
                    csv_file = csv.DictWriter(export_file, attrs.keys())
                    if add_header:
                        csv_file.writeheader()
                        add_header = False
                    csv_file.writerow(attrs)
            except IOError as e:
                print helpers.color("\nCannot export file " + f + ": " + e.strerror)
                return
        print helpers.color("\nExport file saved to disk at " + f)

        return

    def load_ips(self, file_of_systems):

        # Check to make sure file given is a valid file
        if os.path.isfile(file_of_systems):
            # read in IPs from a file
            with open(file_of_systems, "r") as system_file:
                justmetadata_system_list = system_file.readlines()
            total_systems = 0

            # Cast each IP its own object
            for system in justmetadata_system_list:
                if "/" in system:
                    try:
                        for ip in netaddr.IPSet([system]):
                            ip = str(ip)
                            activated_system_object = ip_object.IP_Information(ip)
                            if ip in self.system_objects:
                                self.system_objects[ip][1] = self.system_objects[ip][1] + 1
                                total_systems += 1
                            else:
                                self.system_objects[ip] = [activated_system_object, 1]
                                total_systems += 1
                    except netaddr.core.AddrFormatError:
                        print helpers.color("[*] Error: Bad IP CIDR range detected! (" + str(system).strip() + ")", warning=True)
                        continue
                else:
                    activated_system_object = ip_object.IP_Information(system.strip())
                    if system in self.system_objects:
                        self.system_objects[system][1] = self.system_objects[system][1] + 1
                        total_systems += 1
                    else:
                        self.system_objects[system] = [activated_system_object, 1]
                        total_systems += 1

            print helpers.color("[*] Loaded " + str(total_systems) + " systems")

        else:
            print helpers.color("\n\n[*] Error: Invalid file path provided!", warning=True)
            print helpers.color("[*] Error: Please provide the valid path to a file.", warning=True)
        return

    def load_intelgathering_functions(self):
        for name in glob.glob('modules/intelgathering/*.py'):
            if name.endswith(".py") and ("__init__" not in name):
                loaded_ig = imp.load_source(
                    name.replace("/", ".").rstrip('.py'), name)
                self.intelgathering_transforms[name] = loaded_ig.IntelGather()
        return

    def load_analytical_functions(self):
        for name in glob.glob('modules/analytics/*.py'):
            if name.endswith(".py") and ("__init__" not in name):
                loaded_analytics = imp.load_source(
                    name.replace("/", ".").rstrip('.py'), name)
                self.analytical_transforms[name] = loaded_analytics.Analytics(self.cli_args)
        return

    def menu_system(self):

        # Reinitialize colorama as sys.stdout/stderr may have changed since program started
        colorama.reinit()

        while self.user_command == "":

            try:

                if self.cli_args is not None:

                    if self.cli_args.list is not None:
                        self.run_list_command(self.cli_args.list)

                    if self.cli_args.load is not None:
                        try:
                            self.load_ips(self.cli_args.load)
                        except IndexError:
                            print helpers.color("\n\n[*] Error: Load command requires a path to a file!", warning=True)
                            print helpers.color("[*] Ex: load /root/file.txt", warning=True)
                            sys.exit()

                    if self.cli_args.file_import is not None:
                        self.run_import_command(self.cli_args.file_import)

                    if self.cli_args.gather is not None:
                        self.run_gather_command(self.cli_args.gather)

                    if self.cli_args.save is not None:
                        self.save_state(self.cli_args.save)
                    else:
                        self.save_state("")

                    if self.cli_args.analyze is not None:
                        self.run_analyze_command(self.cli_args.analyze)

                    if self.cli_args.ip_info is not None:
                        self.run_ipinfo_command(self.cli_args.ip_info)

                    if self.cli_args.export is not None:
                        self.export_info(self.cli_args.export)
                    else:
                        self.export_info("")

                    sys.exit()

                else:

                    while True:
                        self.user_command = raw_input(' \n\n[>] Please enter a command: ').strip()
                        helpers.print_header()

                        if self.user_command is not "":
                            # Check if command is to load IP addresses into framework
                            if self.user_command.startswith('load'):
                                try:
                                    self.load_ips(self.user_command.split()[1])
                                except IndexError:
                                    print helpers.color("\n\n[*] Error: Load command requires a path to a file!", warning=True)
                                    print helpers.color("[*] Ex: load /root/file.txt", warning=True)
                                self.user_command = ""

                            elif self.user_command.startswith('gather'):
                                try:
                                    self.run_gather_command(
                                        self.user_command.split()[1])
                                except IndexError:
                                    print helpers.color("\n\n[*] Error: Module command requires a module to load!", warning=True)
                                    print helpers.color("[*] Ex: gather geoinfo", warning=True)
                                self.user_command = ""

                            elif self.user_command.startswith('help'):
                                self.print_commands()
                                self.user_command = ""

                            elif self.user_command.startswith('exit'):
                                print helpers.color(
                                    "\n\n[!] Exiting Just Metadata..\n",
                                    warning=True)
                                sys.exit()

                            # Code for saving current state to disk
                            elif self.user_command.startswith('save'):
                                try:
                                    self.save_state(
                                        self.user_command.split()[1])
                                except:
                                    self.save_state("")
                                self.user_command = ""

                            # Code for loading state from disk
                            elif self.user_command.startswith('import'):
                                try:
                                    self.run_import_command(
                                        self.user_command.split()[1])
                                except IndexError:
                                    print helpers.color("[*] Error: Please provide path to file that will be imported.", warning=True)
                                    print helpers.color("[*] Ex: import metadata1111_1111.state", warning=True)
                                self.user_command = ""

                            elif self.user_command.startswith('ip_info'):
                                try:
                                    self.run_ipinfo_command(
                                        self.user_command.split()[1])
                                except IndexError:
                                    print helpers.color("[*] Error: The \"ip_info\" command requires an IP address!", warning=True)
                                    self.check_cli()
                                self.user_command = ""

                            # This will be the export command, used to export
                            # all information into a csv file
                            elif self.user_command.startswith('export'):
                                try:
                                    self.export_info(
                                        self.user_command.split()[1])
                                except:
                                    self.export_info("")
                                self.user_command = ""

                            elif self.user_command.startswith('analyze'):
                                try:
                                    self.run_analyze_command(
                                        self.user_command.split()[1])
                                except IndexError:
                                    print helpers.color("\n\n[*] Error: Analyze command requires a module to load!", warning=True)
                                    print helpers.color("[*] Ex: analyze GeoInfo", warning=True)
                                self.user_command = ""

                            elif self.user_command.startswith('list'):
                                try:
                                    self.run_list_command(
                                        self.user_command.split()[1])
                                except IndexError:
                                    print helpers.color("\n\n[*] Error: You did not provide a module type to display!", warning=True)
                                    print helpers.color("[*] Ex: list analysis", warning=True)
                                self.user_command = ""

                            else:
                                print helpers.color("\n\n[*] Error: You did not provide a valid command!", warning=True)
                                print helpers.color("[*] Type \"help\" to view valid commands", warning=True)

            except KeyboardInterrupt:
                print helpers.color("\n\n[!] You just rage quit...", warning=True)
                sys.exit()

            #except Exception as e:
            #    print helpers.color("\n\n[!] Encountered Error!", warning=True)
            #    print helpers.color(e)
            #    print helpers.color("[!] Saving state to disk...", warning=True)
            #    print helpers.color("[!] Please report this info to the developer!", warning=True)
            #    self.save_state()

        return

    def print_commands(self):
        # Function used to print all available commands
        print
        for command, description in self.commands.iteritems():
            print command + " => " + description
        return

    def run_analyze_command(self, analyze_command):
        try:
            hit_module = False
            for path, analytics_obj in self.analytical_transforms.iteritems():
                if analyze_command.lower() == 'all':
                    analytics_obj.analyze(self.system_objects)
                    hit_module = True
                elif analyze_command.lower() == analytics_obj.cli_name.lower():
                    analytics_obj.analyze(self.system_objects)
                    hit_module = True
                    break
        except IndexError:
            print helpers.color("\n\n[*] Error: Analyze command requires a module to load!", warning=True)
            print helpers.color("[*] Ex: analyze GeoInfo", warning=True)
            self.check_cli()
        if not hit_module:
            print helpers.color("\n\n[*] Error: You didn't provide a valid module!", warning=True)
            print helpers.color("[*] Please re-run and use a valid module.", warning=True)
            self.check_cli()
        return

    def run_gather_command(self, gather_module):
        gather_module_found = False
        try:
            for path, ig_obj in self.intelgathering_transforms.iteritems():
                if gather_module.lower() == 'all':
                    ig_obj.gather(self.system_objects)
                    gather_module_found = True
                elif gather_module.lower() == ig_obj.cli_name.lower():
                    ig_obj.gather(self.system_objects)
                    gather_module_found = True
                    break
            if not gather_module_found:
                print helpers.color("\n\n[*] Error: You didn't provide a valid gather module!", warning=True)
                print helpers.color("[*] Please re-run and use a valid module.", warning=True)
                self.check_cli()
        except IndexError:
            print helpers.color("\n\n[*] Error: Module command requires a module to load!", warning=True)
            print helpers.color("[*] Ex: gather geoinfo", warning=True)
            self.check_cli()
        except KeyboardInterrupt:
            print helpers.color("\n\n[*] You rage quit your intel gathering!", warning=True)
            self.check_cli()
        return

    def run_import_command(self, import_path):
        try:
            if os.path.isfile(import_path):
                try:
                    self.system_objects = pickle.load(open(import_path, 'rb'))
                    print helpers.color("[*] Successfully imported " + import_path)
                except IndexError:
                    print helpers.color("[*] Error: Invalid state file.", warning=True)
                    print helpers.color("[*] Please provide the path to a valid state file.", warning=True)
                    self.check_cli()
                except KeyError:
                    print helpers.color("[*] Error: Problem parsing your state file.", warning=True)
                    print helpers.color("[*] Error: Has it been tampered with...?", warning=True)
                    self.check_cli()
            else:
                print helpers.color("[*] Error: Please provide path to file that will be imported.", warning=True)
                self.check_cli()
        except IndexError:
            print helpers.color("[*] Error: Please provide path to file that will be imported.", warning=True)
            print helpers.color("[*] Ex: import metadata1111_1111.state", warning=True)
            self.check_cli()
        return

    def run_ipinfo_command(self, ip_addr):
        ip_found = False
        try:
            for path, ip_objd in self.system_objects.iteritems():
                if ip_objd[0].ip_address == ip_addr or ip_addr.lower() == 'all':
                    attrs = vars(ip_objd[0])
                    print ip_objd[0].ip_address
                    print "*" * 25
                    for key, value in attrs.iteritems():
                        print helpers.color(key) + ": " + self.collapse(value)
                    ip_found = True
            if not ip_found:
                print helpers.color("[*] Error: The provided IP address is not loaded in the framework!", warning=True)
                print helpers.color("[*] Error: Please provide a new IP.", warning=True)
                self.check_cli()
        except IndexError:
            print helpers.color("[*] Error: The \"ip_info\" command requires an IP address!", warning=True)
            self.check_cli()
        return

    def run_list_command(self, list_cmd):
        try:
            if len(list_cmd.split()) == 1:
                list_command = list_cmd
            else:
                list_command = list_cmd.split()[1]
            if list_command.lower() == 'analysis':
                for path, object_name in self.analytical_transforms.iteritems():
                    print object_name.cli_name + " => " + object_name.description
                print "All => Invokes all of the above Analysis modules"
            elif list_command.lower() == 'gather':
                for path, object_name in self.intelgathering_transforms.iteritems():
                    print object_name.cli_name + " => " + object_name.description
                print "All => Invokes all of the above IntelGathering modules"
        except IndexError:
            print helpers.color("\n\n[*] Error: You did not provide module type to display!", warning=True)
            print helpers.color("[*] Ex: list analysis", warning=True)
            self.check_cli()
        return

    def save_state(self, f):
        current_date = time.strftime("%m/%d/%Y").replace("/", "")
        current_time = time.strftime("%H:%M:%S").replace(":", "")

        if f == "":
            f = 'metadata' + current_date + "_" + current_time + '.state'

        # Save state to disk
        try:
            pickle.dump(self.system_objects, open(f, 'wb'))
            print helpers.color("\nState saved to disk at " + f)
        except IOError as e:
            print helpers.color("\nCannot save state file " + f + ": " + e.strerror)
            pass
        return