'''
Orthrus commands implementation
'''
import os
import sys
import shutil
import re
import subprocess
import random
import glob
import webbrowser
import tarfile
import time
import json
import string
from orthrusutils import orthrusutils as util
from builder import builder as b
from job import job as j
from spectrum.afl_sancov import AFLSancovReporter
from runtime.runtime import RuntimeAnalyzer

class OrthrusCreate(object):

    def __init__(self, args, config, test=False):
        self.args = args
        self.config = config
        self.test = test
        self.orthrusdir = self.config['orthrus']['directory']
        self.orthrus_subdirs = ['binaries', 'conf', 'logs', 'jobs', 'archive']
        self.fail_msg_bin = "Could not find ELF binaries. While we cannot guarantee " \
                            "that all libraries were instrumented correctly, they most likely were."

    def archive(self):

        util.color_print_singleline(util.bcolors.OKGREEN, "\t\t[?] Rerun create? [y/n]...: ")

        if not self.test and 'y' not in sys.stdin.readline()[0]:
            return False

        if not util.pprint_decorator_fargs(util.func_wrapper(shutil.move, '{}/binaries'.format(self.orthrusdir),
                                            '{}/archive/binaries.{}'.format(self.orthrusdir,
                                                                            time.strftime("%Y-%m-%d-%H:%M:%S"))),
                                           'Archiving binaries to {}/archive'.format(self.orthrusdir), indent=2):
            return False
        return True

    def verifycmd(self, cmd):
        try:
            subprocess.check_output(cmd, shell=True)
        except subprocess.CalledProcessError:
            return False

        return True

    def verifyafl(self, binpath):
        cmd = ['objdump -t ' + binpath + ' | grep __afl_maybe_log']
        return self.verifycmd(cmd)

    def verifyasan(self, binpath):
        cmd = ['objdump -t ' + binpath + ' | grep __asan_get_shadow_mapping']
        return self.verifycmd(cmd)

    def verifyubsan(self, binpath):
        cmd = ['objdump -t ' + binpath + ' | grep ubsan_init']
        return self.verifycmd(cmd)

    def verify_gcccov(self, binpath):
        cmd = ['objdump -t ' + binpath + ' | grep gcov_write_block']
        return self.verifycmd(cmd)

    def verify_sancov(self, binpath):
        cmd = ['objdump -t ' + binpath + ' | grep __sanitizer_cov_module_init']
        return self.verifycmd(cmd)

    def verify_asancov(self, binpath):
        if not (self.verifyasan(binpath) and self.verify_sancov(binpath)):
            return False
        return True

    def verify_ubsancov(self, binpath):
        if not (self.verifyubsan(binpath) and self.verify_sancov(binpath)):
            return False
        return True

    def verify(self, binpath, benv):

        if 'afl' in benv.cc and not self.verifyafl(binpath):
            return False
        if ('-fsanitize=address' in benv.cflags or 'AFL_USE_ASAN=1' in benv.misc) and not self.verifyasan(binpath):
            return False
        if '-ftest-coverage' in benv.cflags and not self.verify_gcccov(binpath):
            return False
        if '-fsanitize-coverage' in benv.cflags and '-fsanitize=address' in benv.cflags and not self.verify_asancov(binpath):
            return False
        if '-fsanitize-coverage' in benv.cflags and '-fsanitize=undefined' in benv.cflags and not self.verify_ubsancov(binpath):
            return False

        return True

    def create(self, dest, BEnv, logfn, gendict=False):

        if not gendict:
            install_path = dest
            util.mkdir_p(install_path)

            ### Configure
            config_flags = ['--prefix=' + os.path.abspath(install_path)] + \
                           self.args.configure_flags.split(" ")
        else:
            config_flags = self.args.configure_flags.split(" ")

        builder = b.Builder(b.BuildEnv(BEnv),
                            config_flags,
                            self.config['orthrus']['directory'] + "/logs/" + logfn)

        if not util.pprint_decorator(builder.configure, 'Configuring', 2):
            return False


        ### Make install
        if not gendict:
            if not util.pprint_decorator(builder.make_install, 'Compiling', 2):
                return False

            util.copy_binaries(install_path + "bin/")

            # Fixes https://github.com/test-pipeline/orthrus/issues/1
            # Soft fail when no ELF binaries found.
            binary_paths = util.return_elf_binaries(install_path + 'bin/')
            if not util.pprint_decorator_fargs(binary_paths, 'Looking for ELF binaries', 2, fail_msg=self.fail_msg_bin):
                return True

            sample_binpath = random.choice(binary_paths)

            if not util.pprint_decorator_fargs(util.func_wrapper(self.verify, sample_binpath, BEnv),
                                         'Verifying instrumentation', 2):
                return False
        else:
            if not util.pprint_decorator(builder.clang_sdict, 'Creating input dict via clang-sdict', 2):
                return False

        return True

    def run(self):

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Creating Orthrus workspace")

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.config['orthrus']['directory']) is False,
                                           'Checking if workspace exists', indent=2,
                                           fail_msg='yes'):
            if not self.archive():
                return False

        util.mkdir_p(self.config['orthrus']['directory'])
        dirs = ['/{}/'.format(x) for x in self.orthrus_subdirs]
        map(lambda x: util.mkdir_p(self.config['orthrus']['directory'] + x), dirs)

        # AFL-ASAN
        if self.args.afl_asan:
            install_path = self.config['orthrus']['directory'] + "/binaries/afl-asan/"

            if not util.pprint_decorator_fargs(util.func_wrapper(self.create, install_path, b.BuildEnv.BEnv_afl_asan,
                                                                 'afl-asan_inst.log'),
                                               'Installing binaries for afl-fuzz with AddressSanitizer',
                                               indent=1):
                return False

            #
            # ASAN Debug 
            #
            install_path = self.config['orthrus']['directory'] + "/binaries/asan-dbg/"
            if not util.pprint_decorator_fargs(util.func_wrapper(self.create, install_path, b.BuildEnv.BEnv_asan_debug,
                                                                 'afl-asan_dbg.log'),
                                               'Installing binaries for debug with AddressSanitizer',
                                               indent=1):
                return False
        # AFL-ASAN-BLACKLIST
        elif self.args.afl_asan_blacklist:

            install_path = self.config['orthrus']['directory'] + "/binaries/afl-asan/"

            is_blacklist = os.path.exists('asan_blacklist.txt')
            if not util.pprint_decorator_fargs(is_blacklist, 'Checking if asan_blacklist.txt exists',
                                               indent=2):
                return False

            if not util.pprint_decorator_fargs(
                    util.func_wrapper(self.create, install_path, b.BuildEnv.BEnv_afl_asan_blacklist,
                                      'afl-asan_inst.log'),
                    'Installing binaries for afl-fuzz with AddressSanitizer (blacklist)',
                    indent=1):
                return False

            #
            # ASAN Debug
            #
            install_path = self.config['orthrus']['directory'] + "/binaries/asan-dbg/"
            if not util.pprint_decorator_fargs(
                    util.func_wrapper(self.create, install_path, b.BuildEnv.BEnv_asan_debug_blacklist,
                                      'afl-asan_dbg.log'),
                    'Installing binaries for debug with AddressSanitizer (blacklist)',
                    indent=1):
                return False

        ### AFL-HARDEN
        if self.args.afl_harden:

            install_path = self.config['orthrus']['directory'] + "/binaries/afl-harden/"
            if not util.pprint_decorator_fargs(util.func_wrapper(self.create, install_path, b.BuildEnv.BEnv_afl_harden,
                                                                 'afl_harden.log'),
                                               'Installing binaries for afl-fuzz in harden mode',
                                               indent=1):
                if not util.pprint_decorator_fargs(util.func_wrapper(self.create, install_path,
                                                                     b.BuildEnv.BEnv_afl_harden_softfail,
                                                                    'afl-harden_soft.log'),
                                                'Retrying without the (sometimes problematic) AFL_HARDEN=1 setting',
                                                indent=1):
                    return False

            #
            # Harden Debug 
            #
            install_path = self.config['orthrus']['directory'] + "/binaries/harden-dbg/"
            if not util.pprint_decorator_fargs(util.func_wrapper(self.create, install_path, b.BuildEnv.BEnv_harden_debug,
                                                                 'afl-harden_dbg.log'),
                                               'Installing binaries for debug in harden mode',
                                               indent=1):
                if not util.pprint_decorator_fargs(util.func_wrapper(self.create, install_path,
                                                                     b.BuildEnv.BEnv_harden_debug_softfail,
                                                                    'afl-harden_dbg_soft.log'),
                                                    'Retrying without FORTIFY compilation flag',
                                                    indent=1):
                    return False

        ### Coverage
        if self.args.coverage:
            install_path = self.config['orthrus']['directory'] + "/binaries/coverage/gcc/"
            if not util.pprint_decorator_fargs(util.func_wrapper(self.create, install_path, b.BuildEnv.BEnv_gcc_coverage,
                                                                 'gcc_coverage.log'),
                                               'Installing binaries for obtaining test coverage information',
                                               indent=1):
                return False

        ### SanitizerCoverage
        if self.args.san_coverage:
            if self.args.afl_asan:
                install_path = self.config['orthrus']['directory'] + "/binaries/coverage/asan/"
                if not util.pprint_decorator_fargs(util.func_wrapper(self.create, install_path,
                                                                     b.BuildEnv.BEnv_asan_coverage,
                                                                    'asan_coverage.log'),
                                                    'Installing binaries for obtaining ASAN coverage',
                                                    indent=1):
                    return False
            if self.args.afl_harden:
                install_path = self.config['orthrus']['directory'] + "/binaries/coverage/ubsan/"
                if not util.pprint_decorator_fargs(util.func_wrapper(self.create, install_path,
                                                                     b.BuildEnv.BEnv_ubsan_coverage,
                                                                    'ubsan_coverage.log'),
                                                    'Installing binaries for obtaining HARDEN coverage (via UBSAN)',
                                                    indent=1):
                    return False

        if self.args.dictionary:
            if not util.pprint_decorator_fargs(util.func_wrapper(self.create, None,
                                                                 b.BuildEnv.BEnv_bear,
                                                                 'bear.log', True),
                                               'Generating input dictionary',
                                               indent=1):
                return False

        return True

class OrthrusAdd(object):
    
    def __init__(self, args, config):
        self._args = args
        self._config = config
        self.orthrusdir = self._config['orthrus']['directory']

    def copy_samples(self, jobroot_dir):
        samplevalid = False

        if os.path.isdir(self._args.sample):
            for dirpath, dirnames, filenames in os.walk(self._args.sample):
                for fn in filenames:
                    fpath = os.path.join(dirpath, fn)
                    if os.path.isfile(fpath):
                        shutil.copy(fpath, jobroot_dir + "/afl-in/")
            if filenames:
                samplevalid = True
        elif os.path.isfile(self._args.sample):
            samplevalid = True
            shutil.copy(self._args.sample, jobroot_dir + "/afl-in/")

        if not samplevalid:
            return False

        return True

    def seed_job(self, rootdir, id):

        if not util.pprint_decorator_fargs(util.func_wrapper(self.copy_samples, rootdir),
                                          'Adding initial samples for job ID [{}]'.format(id), 2,
                                          'seed dir or file invalid. No seeds copied'):
            return False

        return True

    def write_asan_config(self, afl_in, afl_out, jobroot_dir, fuzzer=None, fuzzer_params=None):

        ## Create an afl-utils JSON config for AFL-ASAN fuzzing setting it as slave if AFL-HARDEN target exists
        asanjob_config = {}
        asanjob_config['input'] = afl_in
        asanjob_config['output'] = afl_out
        asanjob_config['target'] = ".orthrus/binaries/afl-asan/bin/{}".format(self.job.target)
        asanjob_config['cmdline'] = self.job.params
        # asanjob_config['file'] = "@@"
        # asanjob_config.set("afl.ctrl", "file", ".orthrus/jobs/" + self.jobId + "/afl-out/.cur_input_asan")
        asanjob_config['timeout'] = "3000+"

        # See: https://github.com/mirrorer/afl/blob/master/docs/notes_for_asan.txt
        asanjob_config['mem_limit'] = "none"
        # if util.is64bit():
        #     asanjob_config['mem_limit'] = "none"
        # else:
        #     asanjob_config['mem_limit'] = "none"

        asanjob_config['session'] = "ASAN"
        # https://github.com/rc0r/afl-utils/issues/34
        # asanjob_config['interactive'] = False

        if os.path.exists(self._config['orthrus']['directory'] + "/binaries/afl-harden"):
            asanjob_config['master_instances'] = 0

        if fuzzer:
            asanjob_config['fuzzer'] = fuzzer

        if fuzzer_params:
            asanjob_config['afl_margs'] = fuzzer_params

        self.write_config(asanjob_config, "{}/asan-job.conf".format(jobroot_dir))

    def write_harden_config(self, afl_in, afl_out, jobroot_dir, fuzzer=None, fuzzer_params=None):
        ## Create an afl-utils JSON config for AFL-HARDEN
        hardenjob_config = {}
        hardenjob_config['input'] = afl_in
        hardenjob_config['output'] = afl_out
        hardenjob_config['target'] = ".orthrus/binaries/afl-harden/bin/{}".format(self.job.target)
        hardenjob_config['cmdline'] = self.job.params
        # hardenjob_config['file'] = "@@"
        hardenjob_config['timeout'] = "3000+"
        hardenjob_config['mem_limit'] = "none"
        hardenjob_config['session'] = "HARDEN"
        # hardenjob_config['interactive'] = False

        if fuzzer:
            hardenjob_config['fuzzer'] = fuzzer

        if fuzzer_params:
            hardenjob_config['afl_margs'] = fuzzer_params

        self.write_config(hardenjob_config, "{}/harden-job.conf".format(jobroot_dir))

    def write_config(self, config_dict, config_file):
        with open(config_file, 'wb') as file:
            json.dump(config_dict, file, indent=4)

    def config_wrapper(self, afl_in, afl_out, jobroot_dir, fuzzer=None, fuzzer_params=None):
        self.write_asan_config(afl_in, afl_out, jobroot_dir, fuzzer, fuzzer_params)
        self.write_harden_config(afl_in, afl_out, jobroot_dir, fuzzer, fuzzer_params)
        return True

    def config_job(self, rootdir, id, fuzzer=None, fuzzer_params=None):
        afl_dirs = [rootdir + '/{}'.format(dirname) for dirname in ['afl-in', 'afl-out']]

        for dir in afl_dirs:
            os.mkdir(dir)

        # HT: http://stackoverflow.com/a/13694053/4712439
        if not util.pprint_decorator_fargs(util.func_wrapper(self.config_wrapper, afl_dirs[0], afl_dirs[1],
                                                             rootdir, fuzzer, fuzzer_params),
                                           'Configuring {} job for ID [{}]'.format(self.jobtype, id), 2):
            return False

        return True

    def extract_job(self, jobroot_dir):
        next_session = 0

        if not tarfile.is_tarfile(self._args._import):
            return False

        if not os.path.exists(jobroot_dir + "/afl-out/"):
            return False

        syncDir = os.listdir(jobroot_dir + "/afl-out/")
        for directory in syncDir:
            if "SESSION" in directory:
                next_session += 1

        is_single = True
        with tarfile.open(self._args._import, "r") as tar:
            try:
                info = tar.getmember("fuzzer_stats")
            except KeyError:
                is_single = False

            if is_single:
                outDir = jobroot_dir + "/afl-out/SESSION" + "{:03d}".format(next_session)
                os.mkdir(outDir)
                tar.extractall(outDir)
            else:
                tmpDir = jobroot_dir + "/tmp/"
                os.mkdir(tmpDir)
                tar.extractall(tmpDir)
                for directory in os.listdir(jobroot_dir + "/tmp/"):
                    outDir = jobroot_dir + '/afl-out/'
                    shutil.move(tmpDir + directory, outDir)
                shutil.rmtree(tmpDir)

        return True

    def import_job(self, rootdir, id, target, params):

        if not util.pprint_decorator_fargs(util.func_wrapper(self.extract_job, rootdir),
                                           'Importing afl sync dir for job ID [{}]'.format(id),
                                           indent=2):
            return False

        util.minimize_and_reseed(self.orthrusdir, rootdir, id, target, params)
        return True

    def run_helper(self, rootdir, id, fuzzer, fuzzer_param):
        if not self.config_job(rootdir, id, fuzzer, fuzzer_param):
            return False
        if self._args._import and not self.import_job(rootdir, id, self.job.target, self.job.params):
            return False
        if self._args.sample and not self.seed_job(rootdir, id):
            return False

        return True

    def run(self):
        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Adding new job to Orthrus workspace")


        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.orthrusdir + "/binaries/"),
                                          "Checking Orthrus workspace", 2,
                                          'failed. Are you sure you did orthrus create -asan or -fuzz'):
            return False

        valid_jobtype = ((self._args.jobtype == 'routine') or (self._args.jobtype == 'abtests'))

        if not util.pprint_decorator_fargs((self._args.jobtype and self._args.jobconf and valid_jobtype),
                                           "Checking job type", 2,
                                           'failed. --jobtype=[routine|abtests] or --jobconf argument missing, or'
                                           ' invalid job type.'):
            return False

        self.jobtype = self._args.jobtype
        self.jobconf = self._args.jobconf

        self.job = j.job(self._args.job, self.jobtype, self.orthrusdir, self.jobconf)

        self.rootdirs = []
        self.ids = []
        self.fuzzers = []
        self.fuzzer_param = []

        if not util.pprint_decorator(self.job.materialize, 'Adding {} job'.format(self.jobtype), 2,
                                     'Invalid a/b test configuration or existing job found!'):
            return False

        if self.jobtype == 'routine':
            self.rootdirs.append(self.job.rootdir)
            self.ids.append(self.job.id)
            self.fuzzers.extend(self.job.fuzzers)
            self.fuzzer_param.extend(self.job.fuzzer_args)
        else:
            self.rootdirs.extend(self.job.rootdir + '/{}'.format(id) for id in self.job.jobids)
            self.ids.extend(self.job.jobids)
            self.fuzzers.extend(self.job.fuzzers)
            self.fuzzer_param.extend(self.job.fuzzer_args)

        for rootdir, id, fuzzer, fuzzer_param in zip(self.rootdirs, self.ids, self.fuzzers, self.fuzzer_param):
            if not self.run_helper(rootdir, id, fuzzer, fuzzer_param):
                return False
        return True

class OrthrusRemove(object):

    fail_msg = "failed. Are you sure you have done orthrus add --job or passed the " \
               "right job ID. orthrus show -j might help"
    
    def __init__(self, args, config):
        self._args = args
        self._config = config
        self.orthrusdir = self._config['orthrus']['directory']
    
    def run(self):

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Removing job ID [{}]".format(self._args.job_id))

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.orthrusdir),
                                          "Checking Orthrus workspace", 2,
                                          'failed. Are you sure you ran orthrus create -asan -fuzz?'):
            return False

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Removing fuzzing job from Orthrus workspace")

        job_token = j.jobtoken(self.orthrusdir, self._args.job_id)
        if not util.pprint_decorator(job_token.materialize, 'Retrieving job [{}]'.format(job_token.id), indent=2,
                                     fail_msg=self.fail_msg):
            return False

        if not util.pprint_decorator_fargs(util.func_wrapper(shutil.move,
                                                             self.orthrusdir + "/jobs/{}/{}".format(job_token.type,
                                                                                                   job_token.id),
                                                             self.orthrusdir + "/archive/" +
                                                                    time.strftime("%Y-%m-%d-%H:%M:%S") + "-"
                                                                    + job_token.id),
                                           'Archiving data for {} job [{}]'.format(job_token.type,job_token.id),
                                           indent=2):
            return False

        j.remove_id_from_conf(job_token.jobsconf, job_token.id, job_token.type)
        return True

class OrthrusStart(object):
    
    def __init__(self, args, config, test=False):
        self._args = args
        self._config = config
        self.test = test
        self.orthrusdir = self._config['orthrus']['directory']
        self.fail_msg = "failed. Are you sure you have done orthrus add --job or passed the " \
                        "right job ID. orthrus show -j might help"
        self.is_harden = os.path.exists(self.orthrusdir + "/binaries/afl-harden")
        self.is_asan = os.path.exists(self.orthrusdir + "/binaries/afl-asan")

    def check_core_pattern(self):
        cmd = ["cat /proc/sys/kernel/core_pattern"]
        util.color_print_singleline(util.bcolors.OKGREEN, "\t\t[+] Checking core_pattern... ")
        try:
            if "core" not in subprocess.check_output(" ".join(cmd), shell=True, stderr=subprocess.STDOUT):
                util.color_print(util.bcolors.FAIL, "failed")
                util.color_print(util.bcolors.FAIL, "\t\t\t[-] Please do echo core | "
                                                    "sudo tee /proc/sys/kernel/core_pattern")
                return False
        except subprocess.CalledProcessError as e:
            print e.output
            return False
        util.color_print(util.bcolors.OKGREEN, "done")

    def print_cmd_diag(self, file):
        output = open(self.orthrusdir + file, "r")
        for line in output:
            if "Starting master" in line or "Starting slave" in line:
                util.color_print(util.bcolors.OKGREEN, "\t\t\t" + line)
            if " Master " in line or " Slave " in line:
                util.color_print_singleline(util.bcolors.OKGREEN, "\t\t\t\t" + "[+] " + line)
        output.close()

    def compute_cores_per_job(self, job_type):
        if job_type == 'routine':
            if self.is_harden and self.is_asan:
                self.core_per_subjob = self.total_cores / 2
            elif (self.is_harden and not self.is_asan) or (not self.is_harden and self.is_asan):
                self.core_per_subjob = self.total_cores
        else:
            if self.is_harden and self.is_asan:
                self.core_per_subjob = self.total_cores / (2 * self.job_token.num_jobs)
            elif (self.is_harden and not self.is_asan) or (not self.is_harden and self.is_asan):
                self.core_per_subjob = self.total_cores / self.job_token.num_jobs

    def _start_fuzzers(self, jobroot_dir, job_type):
        if os.listdir(jobroot_dir + "/afl-out/") == []:
            start_cmd = "start"
            add_cmd = "add"
        else:
            start_cmd = "resume"
            add_cmd = "resume"

        self.check_core_pattern()

        env = os.environ.copy()
        env.update({'AFL_SKIP_CPUFREQ': '1'})

        if self.is_harden and self.is_asan:
            harden_file = self.orthrusdir + "/logs/afl-harden.log"
            cmd = ["afl-multicore", "--config={}".format(jobroot_dir) + "/harden-job.conf",
                   start_cmd, str(self.core_per_subjob), "-v"]

            if not util.pprint_decorator_fargs(util.func_wrapper(util.run_cmd, " ".join(cmd), env, harden_file),
                                               'Starting AFL harden fuzzer as master', indent=2):
                return False

            self.print_cmd_diag("/logs/afl-harden.log")
            
            # if self.is_asan:
            asan_file = self.orthrusdir + "/logs/afl-asan.log"
            cmd = ["afl-multicore", "--config={}".format(jobroot_dir) + "/asan-job.conf ", add_cmd, \
                   str(self.core_per_subjob), "-v"]
            # This ensures SEGV crashes are named sig:11 and not sig:06
            # See: https://groups.google.com/forum/#!topic/afl-users/aklNGdKbpkI
            util.overrride_default_afl_asan_options(env)

            if not util.pprint_decorator_fargs(util.func_wrapper(util.run_cmd, " ".join(cmd), env, asan_file),
                                               'Starting AFL ASAN fuzzer as slave', indent=2):
                return False

            self.print_cmd_diag("/logs/afl-asan.log")

        elif (self.is_harden and not self.is_asan):
            harden_file = self.orthrusdir + "/logs/afl-harden.log"
            cmd = ["afl-multicore", "--config={}".format(jobroot_dir) + "/harden-job.conf",
                   start_cmd, str(self.core_per_subjob), "-v"]

            if not util.pprint_decorator_fargs(util.func_wrapper(util.run_cmd, " ".join(cmd), env, harden_file),
                                               'Starting AFL harden fuzzer as master', indent=2):
                return False

            self.print_cmd_diag("/logs/afl-harden.log")

        elif (not self.is_harden and self.is_asan):

            asan_file = self.orthrusdir + "/logs/afl-asan.log"
            cmd = ["afl-multicore", "--config={}".format(jobroot_dir) + "/asan-job.conf", start_cmd, \
                   str(self.core_per_subjob), "-v"]
            # This ensures SEGV crashes are named sig:11 and not sig:06
            # See: https://groups.google.com/forum/#!topic/afl-users/aklNGdKbpkI
            util.overrride_default_afl_asan_options(env)

            if not util.pprint_decorator_fargs(util.func_wrapper(util.run_cmd, " ".join(cmd), env, asan_file),
                                               'Starting AFL ASAN fuzzer as master', indent=2):
                return False

            self.print_cmd_diag("/logs/afl-asan.log")

        return True

    def start_and_cover(self):

        for rootdir, id in zip(self.rootdirs, self.ids):
            if not util.pprint_decorator_fargs(util.func_wrapper(self._start_fuzzers, rootdir, self.job_token.type),
                                               'Starting fuzzer for {} job ID [{}]'.format(self.job_token.type,id),
                                               indent=2):
                try:
                    subprocess.call("pkill -9 afl-fuzz", shell=True, stderr=subprocess.STDOUT)
                except OSError, subprocess.CalledProcessError:
                    return False
                return False

            # Live coverage is only supported for routine jobs
            # To support live coverage for abtests jobs, we would need to create two code base dir each with a gcno file
            # set due to the way gcov works.
            if self._args.coverage:
                if self.job_token.type == 'routine':
                    if not util.pprint_decorator_fargs(util.func_wrapper(util.run_afl_cov, self.orthrusdir, rootdir,
                                                                        self.job_token.target, self.job_token.params, True,
                                                                        self.test),
                                                       'Starting afl-cov for {} job ID [{}]'.format(self.job_token.type, id),
                                                       indent=2):
                        return False
                else:
                    util.color_print(util.bcolors.WARNING, "\t\t[+] Live coverage for a/b tests is not supported at the"
                                                           " moment")
                    return True
        return True

    def min_and_reseed(self):

        for rootdir, id in zip(self.rootdirs, self.ids):
            if len(os.listdir(rootdir + "/afl-out/")) > 0:

                if not util.pprint_decorator_fargs(util.func_wrapper(util.minimize_and_reseed, self.orthrusdir, rootdir,
                                                                     id, self.job_token.target, self.job_token.params),
                                                   'Minimizing afl sync dir for {} job ID [{}]'.
                                                           format(self.job_token.type,id),
                                                   indent=2):
                    return False
        return True
        
    def run(self):

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Starting fuzzers for job ID [{}]".
                         format(self._args.job_id))

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.orthrusdir),
                                          "Checking Orthrus workspace", 2,
                                          'failed. Are you sure you ran orthrus create -asan -fuzz?'):
            return False

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Starting fuzzing jobs")

        self.job_token = j.jobtoken(self.orthrusdir, self._args.job_id)

        if not util.pprint_decorator(self.job_token.materialize, 'Retrieving job ID [{}]'.format(self.job_token.id),
                                     indent=2, fail_msg=self.fail_msg):
            return False

        self.total_cores = int(util.getnproc())

        '''
        n = number of cores
        Half the cores for AFL Harden (1 Master: n/2 - 1 slave)
        Half the cores for AFL ASAN (n/2 slaves)
        OR if asan only present
        1 AFL ASAN Master, n-1 AFL ASAN slaves

        In a/b test mode, each group has
        Half the cores for AFL Harden (1 Master: n/4 - 1 slave)
        Half the cores for AFL ASAN (n/4 slaves)
        OR if asan only present
        1 AFL ASAN Master, n/2 - 1 AFL ASAN slaves
        '''

        self.compute_cores_per_job(self.job_token.type)

        if self.core_per_subjob == 0:
            self.core_per_subjob = 1
            if self.job_token.type != 'routine':
                util.color_print(util.bcolors.WARNING, "\t\t\t[-] You do not have sufficient processor cores to carry"
                                                       " out a scientific a/b test. Consider a routine job instead.")

        self.rootdirs = []
        self.ids = []

        if self.job_token.type == 'routine':
            self.rootdirs.append(self.job_token.rootdir)
            self.ids.append(self.job_token.id)
        else:
            self.rootdirs.extend(self.job_token.rootdir + '/{}'.format(id) for id in self.job_token.jobids)
            self.ids.extend(self.job_token.jobids)

        if self._args.minimize:
            if not self.min_and_reseed():
                return False

        if not self.start_and_cover():
            return False

        # for rootdir, id in zip(self.rootdirs, self.ids):
        #     if not self.min_and_reseed(rootdir, id):
        #         return False

        return True
    
class OrthrusStop(object):
    
    def __init__(self, args, config, test=False):
        self._args = args
        self._config = config
        self.orthrusdir = self._config['orthrus']['directory']
        self.routinedir = self.orthrusdir + j.ROUTINEDIR
        self.abtestsdir = self.orthrusdir + j.ABTESTSDIR
        self.test = test

    # NOTE: Supported for routine fuzzing jobs only
    def get_afl_cov_pid(self):
        pid_regex = re.compile(r'afl_cov_pid[^\d]+(?P<pid>\d+)')

        jobs_dir = self.routinedir
        jobs_list = os.walk(jobs_dir).next()[1]

        pids = []
        if not jobs_list:
            return pids
        for job in jobs_list:
            dir = jobs_dir + "/" + job + "/afl-out/cov"
            if not os.path.isdir(dir):
                continue
            file = dir + "/afl-cov-status"
            if not os.path.isfile(file):
                continue
            with open(file) as f:
                content = f.readline()
            match = pid_regex.match(content)
            if match:
                pids.append(match.groups()[0])
        return pids

    def kill_fuzzers_test(self):
        if self.job_token.type == 'routine':
            return util.run_cmd("pkill -15 afl-fuzz")
        else:
            for fuzzer in self.job_token.fuzzers:
                # FIXME: Silently failing
                if not util.run_cmd("pkill -15 {}".format(fuzzer)):
                    return True
        return True

    def run_helper(self):

        if self.test:
            if not util.pprint_decorator(self.kill_fuzzers_test, "Stopping {} job for ID [{}]".format(
                                            self.job_token.type, self.job_token.id), indent=2):
                return False
        else:
            kill_cmd = ["afl-multikill -S HARDEN && afl-multikill -S ASAN"]
            if not util.pprint_decorator_fargs(util.func_wrapper(util.run_cmd, kill_cmd),
                                               "Stopping {} job for ID [{}]".format(self.job_token.type,
                                                                                    self.job_token.id),
                                               indent=2):
                return False

        ## Kill afl-cov only for routine jobs
        if self._args.coverage and self.job_token.type == 'routine':
            util.color_print_singleline(util.bcolors.OKGREEN, "\t\t[+] Stopping afl-cov for {} job... ".
                                        format(self.job_token.type))
            for pid in self.get_afl_cov_pid():
                kill_aflcov_cmd = ["kill", "-15", pid]
                if not util.run_cmd(" ".join(kill_aflcov_cmd)):
                    return False
            util.color_print(util.bcolors.OKGREEN, "done")

        return True

    def run(self):

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Stopping fuzzers for job ID [{}]".
                         format(self._args.job_id))

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.orthrusdir),
                                          "Checking Orthrus workspace", 2,
                                          'failed. Are you sure you ran orthrus create -asan -fuzz?'):
            return False

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Stopping fuzzing jobs")
        self.job_token = j.jobtoken(self.orthrusdir, self._args.job_id)

        if not util.pprint_decorator(self.job_token.materialize, 'Retrieving job ID [{}]'.format(self.job_token.id),
                                     indent=2):
            return False

        return self.run_helper()

class OrthrusShow(object):
    
    def __init__(self, args, config, test=False):
        self._args = args
        self._config = config
        self.test = test
        self.orthrusdir = self._config['orthrus']['directory']
        self.jobsconf = self.orthrusdir + j.JOBCONF
        self.routinedir = self.orthrusdir + j.ROUTINEDIR
        self.abtestsdir = self.orthrusdir + j.ABTESTSDIR
        self.fail_msg = "No coverage info found. Have you run orthrus coverage or" \
                                                " orthrus start -c already?"

    def opencov(self, syncDir, job_type, job_id):
        cov_web_indexhtml = syncDir + "/cov/web/index.html"

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, cov_web_indexhtml),
                                       'Opening coverage html for {} job ID [{}]'.format(job_type, job_id),
                                       indent=2, fail_msg=self.fail_msg):
            return False

        if self.test:
            return True

        webbrowser.open_new_tab(cov_web_indexhtml)
        return True

    # TODO: Add feature
    # def opencov_abtests(self):
    #
    #     control_sync = '{}/{}/afl-out'.format(self.job_token.rootdir, self.job_token.joba_id)
    #     exp_sync = '{}/{}/afl-out'.format(self.job_token.rootdir, self.job_token.jobb_id)
    #
    #     if not self.opencov(control_sync, self.job_token.type, self.job_token.joba_id):
    #         return False
    #     if not self.opencov(exp_sync, self.job_token.type, self.job_token.jobb_id):
    #         return False
    #     return True

    def whatsup(self, syncDir):
        try:
            output = subprocess.check_output(["afl-whatsup", "-s", syncDir])
        except subprocess.CalledProcessError as e:
            print e.output
            return False
        output = output[output.find("==\n\n") + 4:]

        for line in output.splitlines():
            util.color_print(util.bcolors.OKBLUE, "\t" + line)
        triaged_unique = 0

        unique_dir = syncDir + "/../unique"
        if os.path.exists(unique_dir):
            triaged_unique = len(glob.glob(unique_dir + "/asan/*id*sig*")) + \
                             len(glob.glob(unique_dir + "/harden/*id*sig*"))
        util.color_print(util.bcolors.OKBLUE, "\t     Triaged crashes : " + str(triaged_unique))
        return True

    def whatsup_abtests(self, sync_list):
        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "Multivariate test status")
        for idx, val in enumerate(sync_list):
            util.color_print(util.bcolors.OKBLUE, "Config {} [{}]".format(idx, self.job_token.jobids[idx]))
            if not self.whatsup(val):
                return False
        return True


    def show_job(self):
        self.job_token = j.jobtoken(self.orthrusdir, self._args.job_id)

        if not util.pprint_decorator(self.job_token.materialize, 'Retrieving job ID [{}]'.format(self.job_token.id),
                                     indent=2):
            return False

        if self.job_token.type == 'routine':
            return self.whatsup('{}/afl-out'.format(self.job_token.rootdir))
        else:
            return self.whatsup_abtests('{}/{}/afl-out'.format(self.job_token.rootdir, id) for id in
                                        self.job_token.jobids)

    def show_conf(self):
        with open(self.jobsconf, 'r') as jobconf_fp:
            jobsconf_dict = json.load(jobconf_fp)

        self.routine_list = jobsconf_dict['routine']
        self.abtest_list = jobsconf_dict['abtests']
        # self.routine_syncdirs = ['{}/{}'.format(self.routinedir,item['id']) for item in self.routine_list]
        # self.abtest_rootdirs = ['{}/{}'.format(self.abtestsdir,item['id']) for item in self.abtest_list]

        for idx, routine in enumerate(self.routine_list):
            util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "Configured routine jobs:")
            util.color_print(util.bcolors.OKBLUE, "\t" + str(idx) + ") [" + routine['id'] + "] " +
                             routine['target'] + " " + routine['params'])
        for idx, abtest in enumerate(self.abtest_list):
            util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "Configured multivariate tests:")
            for i in range(0, abtest['num_jobs']):
                alp_idx = string.ascii_uppercase[i]
                util.color_print(util.bcolors.OKBLUE, "\t" + str(idx) + ") [" + abtest['id'] + "] " +
                                 abtest['target'] + " " + abtest['params'])
                util.color_print(util.bcolors.OKBLUE, "\t" + "Config {} [{}]".format(i, abtest['jobids'][i]))
                util.color_print(util.bcolors.OKBLUE, "\t" + "Fuzzer {}: {}\t Fuzzer A args: {}".
                                 format(alp_idx, abtest['fuzzers'][i],abtest['fuzzer_args'][i]))
        return True

    def show_cov(self):
        # We have already processed the job
        if self.job_token.type == 'routine':
            util.color_print(util.bcolors.OKGREEN, "\t[+] Opening coverage in new browser tabs")
            return self.opencov('{}/afl-out'.format(self.job_token.rootdir), self.job_token.type, self.job_token.id)
        else:
            util.color_print(util.bcolors.WARNING, "\t[+] Coverage interface for A/B tests is not supported at the "
                                                   "moment")
            return True
            # util.color_print(util.bcolors.OKGREEN, "\t[+] Opening A/B test coverage in new browser tabs")
            # return self.opencov_abtests()

    def run(self):

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Checking stats and config")

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.orthrusdir),
                                          "Checking Orthrus workspace", 2,
                                          'failed. Are you sure you ran orthrus create -asan -fuzz?'):
            return False

        if self._args.job_id:
            if not self.show_job():
                return False
            if self._args.cov and not self.show_cov():
                return False
        elif self._args.conf:
            return self.show_conf()
        return True

class OrthrusTriage(object):
    
    def __init__(self, args, config, test=False):
        self._args = args
        self._config = config
        self.orthrusdir = self._config['orthrus']['directory']
        self.fail_msg = "failed. Are you sure you have done orthrus add --job or passed the " \
                         "right job ID? orthrus show -j might help."
        self.fail_msg_asan = 'No ASAN binary found. Triage requires an ASAN binary to continue. Please do orthrus ' \
                             'create -asan'
        self.is_harden = os.path.exists(self.orthrusdir + "/binaries/afl-harden")
        self.is_asan = os.path.exists(self.orthrusdir + "/binaries/afl-asan")
        self.jobsconf = self.orthrusdir + j.JOBCONF
        self.routinedir = self.orthrusdir + j.ROUTINEDIR
        self.abtestsdir = self.orthrusdir + j.ABTESTSDIR
        self.test = test

    def tidy(self, crash_dir):

        dest = crash_dir + "/.scripts"
        if not os.path.exists(dest):
            os.mkdir(dest)

        for script in glob.glob(crash_dir + "/gdb_script*"):
            shutil.copy(script, dest)
            os.remove(script)

        return True

    def triage(self, jobroot_dir, inst, indir=None, outdir=None):
        env = os.environ.copy()
        util.triage_asan_options(env)

        if inst is 'harden':
            prefix = 'HARDEN'
        elif inst is 'asan' or inst is 'all':
            prefix = 'ASAN'
            inst = 'asan'
        else:
            util.color_print(util.bcolors.FAIL, "failed!")
            return False

        if not indir:
            syncDir = jobroot_dir + "/afl-out/"
        else:
            syncDir = indir

        if not outdir:
            dirname = jobroot_dir + "/exploitable/" + "{}/".format(prefix) + "crashes"
            if not os.path.exists(dirname):
                os.makedirs(dirname)
            triage_outDir = dirname
        else:
            triage_outDir = outdir

        logfile = self.orthrusdir + "/logs/" + "afl-{}_dbg.log".format(inst)
        launch = self.orthrusdir + "/binaries/{}-dbg/bin/".format(inst) + \
                 self.job_token.target + " " + \
                 self.job_token.params.replace("&", "\&")
        cmd = " ".join(["afl-collect", "-r", "-j", util.getnproc(), "-e gdb_script",
                        syncDir, triage_outDir, "--", launch])

        if not util.pprint_decorator_fargs(util.func_wrapper(util.run_cmd, "ulimit -c 0; " + cmd, env, logfile),
                                           'Triaging {} job ID [{}]'.format(self.job_token.type, self.job_token.id),
                                           indent=2):
            return False

        if not util.pprint_decorator_fargs(util.func_wrapper(self.tidy, triage_outDir), 'Tidying crash dir',
                                           indent=2):
            return False

        return True

    def prepare_for_rerun(self, jobroot_dir):
        util.color_print_singleline(util.bcolors.OKGREEN, "[?] Rerun triaging? [y/n]...: ")

        if not self.test and 'y' not in sys.stdin.readline()[0]:
            return False

        shutil.move(jobroot_dir + "/unique/", jobroot_dir + "/unique." + time.strftime("%Y-%m-%d-%H:%M:%S"))
        os.mkdir(jobroot_dir + "/unique/")
        # Archive exploitable crashes from prior triaging if necessary
        exp_path = jobroot_dir + "/exploitable"
        if os.path.exists(exp_path):
            shutil.move(exp_path, "{}.{}".format(exp_path, time.strftime("%Y-%m-%d-%H:%M:%S")))
            os.mkdir(exp_path)
        return True

    def make_unique_dirs(self, jobroot_dir):
        unique_dir = '{}/unique'.format(jobroot_dir)
        if not os.path.exists(unique_dir):
            os.mkdir(unique_dir)
            return True
        else:
            return False

    def get_formatted_crashnames(self, path, prefix):
        list = glob.glob('{}/{}/crashes/*id*sig*'.format(path, prefix))
        ## Rename files
        for file in list:
            head, fn = os.path.split(file)
            newfn = '{}:{}'.format(prefix, fn)
            shutil.move(file, os.path.join(head, newfn))
        return glob.glob('{}/{}/crashes/*id*sig*'.format(path, prefix))


    def triage_wrapper(self, jobroot_dir, job_id):
        if not self.make_unique_dirs(jobroot_dir) and not self.prepare_for_rerun(jobroot_dir):
            return False


        if self.is_harden:
            if not util.pprint_decorator_fargs(util.func_wrapper(self.triage, jobroot_dir, 'harden'),
                                               'Triaging harden mode crashes for {} job ID [{}]'.format(
                                                   self.job_token.type, job_id), indent=2):
                return False

        if self.is_asan:
            if not util.pprint_decorator_fargs(util.func_wrapper(self.triage, jobroot_dir, 'asan'),
                                               'Triaging asan mode crashes for {} job ID [{}]'.format(
                                                   self.job_token.type, job_id), indent=2):
                return False

        # BUGFIX: Second pass may be suboptimal (eliminate HARDEN crashes). Instead simply copy all.
        exp_path = jobroot_dir + "/exploitable"
        uniq_path = jobroot_dir + "/unique"
        if os.path.exists(exp_path):
            exp_all = []
            exp_asan_crashes = self.get_formatted_crashnames(exp_path, 'ASAN')
            exp_harden_crashes = self.get_formatted_crashnames(exp_path, 'HARDEN')
            exp_all.extend(exp_asan_crashes)
            exp_all.extend(exp_harden_crashes)
            for file in exp_all:
                shutil.copy(file, uniq_path)

        triaged_crashes = glob.glob(uniq_path + "/*id*sig*")
        util.color_print(util.bcolors.OKGREEN, "\t\t[+] Triaged " + str(len(triaged_crashes)) + \
                         " crashes. See {}".format(uniq_path))
        if not triaged_crashes:
            util.color_print(util.bcolors.OKBLUE, "\t\t[+] Nothing to do")
            return True

        # Organize unique crashes
        asan_crashes = glob.glob('{}/ASAN*id*sig*'.format(uniq_path))
        harden_crashes = glob.glob('{}/HARDEN*id*sig*'.format(uniq_path))
        if asan_crashes:
            uniq_asan_dir = '{}/asan'.format(uniq_path)
            util.mkdir_p(uniq_asan_dir)
            for file in asan_crashes:
                shutil.move(file, uniq_asan_dir)
        if harden_crashes:
            uniq_harden_dir = '{}/harden'.format(uniq_path)
            util.mkdir_p(uniq_harden_dir)
            for file in harden_crashes:
                shutil.move(file, uniq_harden_dir)

        return True

    def triage_abtests(self):
        count = 0
        for rootdir,id in [('{}/{}'.format(self.job_token.rootdir, jobId), jobId) for jobId in self.job_token.jobids]:
            group = 'Config {}'.format(count)
            count += 1
            if not util.pprint_decorator_fargs(util.func_wrapper(self.triage_wrapper, rootdir, id),
                                               'Triaging crashes in {}'.format(group), indent=2):
                return False

        return True

    def run(self):

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Triaging crashes for job ID [{}]".format(
            self._args.job_id))

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.orthrusdir),
                                          "Checking Orthrus workspace", 2,
                                          'failed. Are you sure you ran orthrus create -asan -fuzz?'):
            return False

        # if not util.pprint_decorator_fargs(self.is_asan, 'Looking for ASAN debug binary', indent=2,
        #                                    fail_msg=self.fail_msg_asan):
        #     return False

        self.job_token = j.jobtoken(self.orthrusdir, self._args.job_id)

        if not util.pprint_decorator(self.job_token.materialize, 'Retrieving job ID [{}]'.format(self.job_token.id),
                                     indent=2, fail_msg=self.fail_msg):
            return False

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Triaging crashes for {} job ID [{}]".format(
            self.job_token.type, self.job_token.id))

        if self.job_token.type == 'routine':
            return self.triage_wrapper(self.job_token.rootdir, self.job_token.id)
            # return self.triage_routine()
        else:
            return self.triage_abtests()

class OrthrusCoverage(object):

    def __init__(self, args, config):
        self._args = args
        self._config = config
        self.orthrusdir = self._config['orthrus']['directory']
        self.fail_msg = "failed. Are you sure you have done orthrus add --job or passed the " \
                         "right job ID? orthrus show -j might help."

    def run(self):

        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Checking coverage for job ID [{}]".format(
            self._args.job_id))

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.orthrusdir),
                                          "Checking Orthrus workspace", 2,
                                          'failed. Are you sure you ran orthrus create -asan -fuzz?'):
            return False

        self.job_token = j.jobtoken(self.orthrusdir, self._args.job_id)

        if not util.pprint_decorator(self.job_token.materialize, 'Retrieving job ID [{}]'.format(self.job_token.id),
                                     indent=2, fail_msg=self.fail_msg):
            return False

        if self.job_token.type == 'abtests':
            util.color_print(util.bcolors.WARNING, "\t[+] Coverage interface for A/B tests is not supported at the "
                                                   "moment")
            return True

        util.color_print(util.bcolors.OKGREEN, "\t\t[+] Checking test coverage for {} job ID [{}]".format(
            self.job_token.type, self.job_token.id))

        #run_afl_cov(orthrus_dir, jobroot_dir, target_arg, params, livemode=False, test=False):
        util.run_afl_cov(self.orthrusdir, self.job_token.rootdir, self.job_token.target, self.job_token.params)

        util.color_print(util.bcolors.OKGREEN, "\t\t[+] This might take a while. Please check {} for progress."
                         .format(self.job_token.rootdir + "/afl-out/cov/afl-cov.log"))
        return True

class OrthrusSpectrum(object):

    def __init__(self, args, config):
        self._args = args
        self._config = config
        self.orthrusdir = self._config['orthrus']['directory']
        self.fail_msg = "failed. Are you sure you have done orthrus add --job or passed the " \
                         "right job ID? orthrus show -j might help."
        self.is_harden = os.path.exists(self.orthrusdir + "/binaries/coverage/ubsan")
        self.is_asan = os.path.exists(self.orthrusdir + "/binaries/coverage/asan")

    def run_afl_sancov(self, is_asan=False):
        if is_asan:
            bin_path = self.orthrusdir + "/binaries/coverage/asan/bin/{}".format(self.job_token.target)
            crash_dir = self.job_token.rootdir + "/unique/asan"
            sanitizer = 'asan'
            target = self.orthrusdir + "/binaries/coverage/asan/bin/" + \
                     self.job_token.target + " " + self.job_token.params.replace("@@", "AFL_FILE")
        else:
            bin_path = self.orthrusdir + "/binaries/coverage/ubsan/bin/{}".format(self.job_token.target)
            crash_dir = self.job_token.rootdir + "/unique/harden"
            sanitizer = 'ubsan'
            target = self.orthrusdir + "/binaries/coverage/ubsan/bin/" + \
                     self.job_token.target + " " + self.job_token.params.replace("@@", "AFL_FILE")

        # def __init__(self, parsed_args, cov_cmd, bin_path, crash_dir, afl_out, sanitizer):
        reporter = AFLSancovReporter(self._args, target, bin_path, crash_dir, '{}/afl-out'.format(self.job_token.rootdir),
                                     sanitizer)
        return reporter.run()

    def run(self):
        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Starting spectrum generation for job ID [{}]".format(
            self._args.job_id))

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.orthrusdir),
                                           "Checking Orthrus workspace", 2,
                                           'failed. Are you sure you ran orthrus create -asan -fuzz?'):
            return False

        self.job_token = j.jobtoken(self.orthrusdir, self._args.job_id)

        if not util.pprint_decorator(self.job_token.materialize, 'Retrieving job ID [{}]'.format(self.job_token.id),
                                     indent=2, fail_msg=self.fail_msg):
            return False

        if self.job_token.type == 'abtests':
            util.color_print(util.bcolors.WARNING, "\t[+] Spectrum generation for A/B tests is not supported at the "
                                                   "moment")
            return True

        if self._args.version:
            reporter = AFLSancovReporter(self._args, None, None, None, None, None)
            if reporter.run():
                return False
            return True

        self.crash_dir = '{}/unique'.format(self.job_token.rootdir)
        if not os.path.exists(self.crash_dir):
            util.color_print(util.bcolors.WARNING, "\t\t[+] It looks like you are attempting to generate crash spectrum "
                                                   "before crash triage. Please triage first.")
            return False

        self.asan_crashes = glob.glob('{}/asan/*id*sig*'.format(self.crash_dir))
        self.harden_crashes = glob.glob('{}/harden/*id*sig*'.format(self.crash_dir))

        if not self.asan_crashes and not self.harden_crashes:
            util.color_print(util.bcolors.INFO, "\t\t[+] There are no crashes to analyze!")
            return True

        if (self.asan_crashes and not self.is_asan) or (self.harden_crashes and not self.is_harden):
            util.color_print(util.bcolors.WARNING, "\t\t[+] It looks like you are attempting to generate crash spectrum "
                                                   "without sanitizer coverage binaries. Did you run orthrus create "
                                                   "with -sancov argument?")
            return False

        self.is_second_run = os.path.exists('{}/crash-analysis/spectrum'.format(self.job_token.rootdir))
        if self.is_second_run and not self._args.overwrite:
            util.color_print(util.bcolors.WARNING, "\t\t[+] It looks like crash spectrum has already been generated. "
                                                   "Please pass --overwrite to regenerate. Old data will be lost unless "
                                                   "manually archived.")
            return False

        util.color_print(util.bcolors.OKGREEN, "\t\t[+] Generating crash spectrum {} job ID [{}]".format(
            self.job_token.type, self.job_token.id))

        if self.asan_crashes:
            if self.run_afl_sancov(True):
               return False
        if self.harden_crashes:
            if self.run_afl_sancov():
                return False

        return True

class OrthrusRuntime(object):

    def __init__(self, args, config):
        self._args = args
        self._config = config
        self.orthrusdir = self._config['orthrus']['directory']
        self.is_harden = os.path.exists(self.orthrusdir + "/binaries/harden-dbg")
        self.is_asan = os.path.exists(self.orthrusdir + "/binaries/asan-dbg")
        self.fail_msg = "failed. Are you sure you have done orthrus add --job or passed the " \
                         "right job ID? orthrus show -j might help."

    def analyze(self, job_rootdir, is_asan=False):
        if is_asan:
            bin_path = self.orthrusdir + "/binaries/asan-dbg/bin/{}".format(self.job_token.target)
            crash_dir = job_rootdir + "/unique/asan"
            sanitizer = 'asan'
            target = self.orthrusdir + "/binaries/asan-dbg/bin/" + \
                     self.job_token.target + " " + self.job_token.params
        else:
            bin_path = self.orthrusdir + "/binaries/harden-dbg/bin/{}".format(self.job_token.target)
            crash_dir = job_rootdir + "/unique/harden"
            sanitizer = 'harden'
            target = self.orthrusdir + "/binaries/harden-dbg/bin/" + \
                     self.job_token.target + " " + self.job_token.params

        #__init__(self, job_token, crash_dir, sanitizer)
        analyzer = RuntimeAnalyzer(job_rootdir, bin_path, target, crash_dir, sanitizer)
        return analyzer.run()

    def analyze_wrapper(self, job_rootdir, job_id):
        crash_dir = '{}/unique'.format(job_rootdir)
        if not os.path.exists(crash_dir):
            util.color_print(util.bcolors.WARNING, "\t\t[+] It looks like you are attempting to analyze crashes you don't "
                                                   "have or not triaged. Please run triage!")
            return False

        asan_crashes = glob.glob('{}/asan/*id*sig*'.format(crash_dir))
        harden_crashes = glob.glob('{}/harden/*id*sig*'.format(crash_dir))

        if not asan_crashes and not harden_crashes:
            util.color_print(util.bcolors.INFO, "\t\t[+] There are no crashes to analyze!")
            return True

        if (asan_crashes and not self.is_asan) or (harden_crashes and not self.is_harden):
            util.color_print(util.bcolors.WARNING, "\t\t[+] It looks like you are attempting to invoke crash analysis "
                                                   "without sanitizer and/or debug binaries. Did you run orthrus create "
                                                   "with -asan -fuzz?")
            return False

        runtime_path = '{}/crash-analysis/runtime'.format(job_rootdir)
        is_second_run = os.path.exists(runtime_path)
        if is_second_run:
            if not self._args.regenerate:
                util.color_print(util.bcolors.WARNING, "\t\t[+] It looks like dynamic analysis results are already there. "
                                                       "Please pass --regenerate to regenerate. Old data will be archived.")
                return False
            else:
                util.color_print(util.bcolors.OKGREEN, "\t\t[+] Archiving old analysis results.")
                shutil.move(runtime_path, "{}.{}".format(runtime_path, time.strftime("%Y-%m-%d-%H:%M:%S")))

        util.color_print(util.bcolors.OKGREEN, "\t\t[+] Performing dynamic analysis of crashes for {} job ID [{}]".format(
            self.job_token.type, job_id))

        if asan_crashes:
            if not self.analyze(job_rootdir, True):
               return False
        if harden_crashes:
            if not self.analyze(job_rootdir):
                return False
        return True

    def runtime_abtests(self):
        count = 0
        for rootdir, id in [('{}/{}'.format(self.job_token.rootdir, jobId), jobId) for jobId in self.job_token.jobids]:
            group = 'Config {}'.format(count)
            count += 1

            if not util.pprint_decorator_fargs(util.func_wrapper(self.analyze_wrapper, rootdir, id),
                                               'Analyzing crashes in {} group'.format(group), indent=2):
                return False

        return True

    def run(self):
        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Starting dynamic analysis of all crashes for"
                                                                  " job ID [{}]".format(self._args.job_id))

        if not util.pprint_decorator_fargs(util.func_wrapper(os.path.exists, self.orthrusdir),
                                           "Checking Orthrus workspace", 2,
                                           'failed. Are you sure you ran orthrus create -asan -fuzz?'):
            return False

        self.job_token = j.jobtoken(self.orthrusdir, self._args.job_id)

        if not util.pprint_decorator(self.job_token.materialize, 'Retrieving job ID [{}]'.format(self.job_token.id),
                                     indent=2, fail_msg=self.fail_msg):
            return False

        if self.job_token.type == 'abtests':
            return self.runtime_abtests()
        else:
            return self.analyze_wrapper(self.job_token.rootdir, self.job_token.id)

class OrthrusDestroy(object):
    
    def __init__(self, args, config, testinput=None):
        self._args = args
        self._config = config
        self.testinput = testinput
        self.orthrusdir = self._config['orthrus']['directory']
    
    def run(self):
        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Destroy Orthrus workspace")
        util.color_print_singleline(util.bcolors.OKGREEN, "\t[?] Delete complete workspace? [y/n]...: ")

        if (self.testinput and 'y' in self.testinput) or 'y' in sys.stdin.readline()[0]:

            if not util.pprint_decorator_fargs(util.func_wrapper(shutil.rmtree, self.orthrusdir),
                                               'Deleting workspace', indent=2):
                return False

        return True

class OrthrusValidate(object):

    def __init__(self, args, config):
        self._args = args
        self._config = config
        self.success_msg = "\t\t[+] All requirements met. Orthrus is ready for use!"

    def get_on(self):
        return [item for item in self._config['dependencies'] if item[1] == 'on']

    def run(self):
        util.color_print(util.bcolors.BOLD + util.bcolors.HEADER, "[+] Validating Orthrus dependencies")
        util.color_print(util.bcolors.OKGREEN, "\t\t[+] The following programs have been marked as required in " \
                                               "~/.orthrus/orthrus.conf")
        for prog, _ in self.get_on():
            util.color_print(util.bcolors.OKGREEN, "\t\t\t[+] {}".format(prog))

        if not util.pprint_decorator_fargs(util.func_wrapper(util.validate_inst, self._config),
                                           'Checking if requirements are met', indent=2):
            return False
        util.color_print(util.bcolors.OKGREEN, self.success_msg)
        return True