def __init__(self, service, categories):
     """
     :param service: Service name
     :param categories: List of categories used to classify the various checks
     """
     self.service    = service
     self.categories = categories
     self.checks     = OrderedDefaultDict(list, {k:[] for k in categories}) # {category: [checks]}
Beispiel #2
0
    def __init__(self, service, categories):
        """
        Construct ServiceChecks object.

        :param str service: Service name
        :param list categories: Categories used to classify the various checks
        """
        self.service = service
        self.categories = categories
        # Organize checks in dict {category: [checks]}
        self.checks = OrderedDefaultDict(list, {k:[] for k in categories})
Beispiel #3
0
    def __init__(self, settings, services):
        """
        Construct the Toolbox object.

        :param Settings settings: Settings from config file
        :param list services: Supported services (including special service "multi")
        """
        self.settings = settings
        self.services = services
        # Organize tools in dict {service: [tools]}
        self.tools = OrderedDefaultDict(list, {k: [] for k in services})
Beispiel #4
0
 def __init__(self, list_services):
     """
     Initialize ServicesConfig with a list of service names
     :param list_services: List of service names
     """
     self.services = OrderedDefaultDict(
         list,
         {
             k: {
                 'default_port': None,
                 'protocol': None,
                 'specific_options':
                 dict(),  # Dictionary { specific option : type }
                 'supported_list_options': dict(
                 ),  # Dictionary { specific option : list of possible values }
                 'auth_types': None,
                 'checks': None,
             }
             for k in list_services
         })
Beispiel #5
0
    def __init__(self, list_services):
        """
        Initialize with list of supported service names

        :param list list_services: List of service names
        """
        self.services = OrderedDefaultDict(
            list,
            {
                k: {
                    'default_port': None,
                    'protocol': None,
                    'specific_options': dict(),  # { specific option : type }
                    'supported_list_options':
                    dict(),  # { specific option : [ values ] }
                    'products': dict(),  # { product type : [ product names ] }
                    'auth_types': None,
                    'checks': None,
                }
                for k in list_services
            })
Beispiel #6
0
class ServiceChecks:
    """All Security Checks for a Service"""

    def __init__(self, service, categories):
        """
        Construct ServiceChecks object.

        :param str service: Service name
        :param list categories: Categories used to classify the various checks
        """
        self.service = service
        self.categories = categories
        # Organize checks in dict {category: [checks]}
        self.checks = OrderedDefaultDict(list, {k:[] for k in categories})


    #------------------------------------------------------------------------------------
    # Basic Operations

    def add_check(self, check):
        """
        Add a Check.

        :param Check check: The check to add
        :return: Status
        :rtype: bool
        """
        self.checks[check.category].append(check)
        return True


    def get_check(self, checkname):
        """
        Get a check by name (NOT case-sensitive).

        :param checkname: Name of the check to get
        :return: Check if found, None otherwise
        :rtype: Check|None
        """
        for cat in self.checks:
            for c in self.checks[cat]:
                if c.name.lower() == checkname.lower():
                    return c
        return None


    def get_all_check_names(self):
        """
        Get list of names of all checks.

        :return: Names of all checks
        :rtype: list
        """
        return [item.name for sublist in self.checks.values() for item in sublist]


    def is_existing_check(self, checkname):
        """
        Indicates if a given check name is existing for the current service 
        (NOT case-sensitive)

        :param checkname: Name of the check to look for
        :return: Result of the search
        :rtype: bool
        """
        return checkname.lower() in map(lambda x: x.lower(), self.get_all_check_names())


    def nb_checks(self):
        """
        Get the total number of checks

        :return: Number of checks
        :rtype: int
        """
        nb = 0
        for category in self.categories:
            nb += len(self.checks[category])
        return nb


    #------------------------------------------------------------------------------------
    # Run 

    def run(self, 
            target, 
            arguments,
            sqlsession,
            filter_categories=None, 
            filter_checks=None, 
            attack_profile=None,
            fast_mode=False,
            attack_progress=None):
        """
        Run checks for the service.
        By default, all the checks are run (but commands are actually run only if 
        target complies with context requirements). It is however possible to apply 
        filters to select the checks to run:
            - Filter on categories,
            - Filter on names of checks.

        :param Target target: Target
        :param ArgumentsParser arguments: Arguments from command-line
        :param Session sqlsession: SQLAlchemy session
        :param list filter_categories: Selection of categories to run (default: all)
        :param list filter_checks: Selection of checks to run (default: all)
        :param AttackProfile attack_profile: Attack profile (default: no profile)
        :param bool fast_mode: Set to true to disable prompts
        :param enlighten.Counter attack_progress: Attack progress
        """

        # # Important: We must keep the order of categories
        # categories = sorted(filter_categories, key=self.categories.index)
        if filter_categories is None:
            filter_categories = self.categories

        # Standard mode 
        # Selected/all categories of checks are run
        if filter_checks is None and attack_profile is None:
            self.__run_standard_mode(target,
                                     arguments,
                                     sqlsession,
                                     filter_categories,
                                     fast_mode,
                                     attack_progress)

        # Special mode
        # User has provided either an attack profile or a list of checks to run 
        # (may be one single check)
        else:

             self.__run_special_mode(target, 
                                     arguments,
                                     sqlsession,
                                     filter_checks,
                                     attack_profile,
                                     fast_mode,
                                     attack_progress)

        return


    def __run_standard_mode(self,
                            target, 
                            arguments,
                            sqlsession,
                            filter_categories, 
                            fast_mode=False,
                            attack_progress=None):
        """
        Run checks for the service in standard mode, i.e. when all or a subset of
        categories of checks must be run against the target.

        :param Target target: Target
        :param ArgumentsParser arguments: Arguments from command-line
        :param Session sqlsession: SQLAlchemy session
        :param list categories: Sorted list of categories to run
        :param enlighten.Counter attack_progress: Attack progress
        """

        # logger.info('Categories of checks that will be run: {cats}'.format(
        #     cats=', '.join(categories)))

        nb_checks = self.nb_checks()

        # Initialize sub status/progress bar
        checks_progress = manager.counter(total=nb_checks+1, 
                                          desc='', 
                                          unit='check',
                                          leave=False,
                                          bar_format=STATUSBAR_FORMAT)
        time.sleep(.5) # hack for progress bar display

        j = 1
        for category in self.categories:
            # Apply filter on categories
            if category not in filter_categories:
                continue

            Output.title1('Category > {cat}'.format(cat=category.capitalize()))

            i = 1
            for check in self.checks[category]:

                # Update status/progress bar
                status = ' +--> Current check [{cur}/{total}]: {category} > ' \
                    '{checkname}'.format(
                        cur       = j,
                        total     = nb_checks,
                        category  = check.category,
                        checkname = check.name)

                checks_progress.desc = '{status}{fill}'.format(
                    status = status,
                    fill   = ' '*(DESC_LENGTH-len(status)))
                checks_progress.update()
                if attack_progress:
                    # Hack to refresh the attack progress bar without incrementing
                    # useful if the tool run during the check has cleared the screen
                    attack_progress.refresh()


                # Run the check if and only if:
                #   - The check has not been already run for this target (except 
                #       if --recheck is specified in command-line)
                #   - Target is compliant with the check,
                #   - The tool used for the check is well installed.
                if i > 1: print()
                
                results_req = ResultsRequester(sqlsession)
                results_req.select_mission(target.service.host.mission.name)
                filter_ = Filter(FilterOperator.AND)
                filter_.add_condition(Condition(target.service.id, 
                    FilterData.SERVICE_ID))
                filter_.add_condition(Condition(check.name, FilterData.CHECK_NAME))
                results_req.add_filter(filter_)
                result = results_req.get_first_result()

                if result is None or arguments.args.recheck == True:

                    if check.check_target_compliance(target):
                        Output.title2('[{category}][Check {num:02}/{total:02}] ' \
                            '{name} > {description}'.format(
                                category    = category.capitalize(),
                                num         = j,
                                total       = nb_checks,
                                name        = check.name,
                                description = check.description))

                        if not check.tool.installed:
                            logger.warning('Skipped: the tool "{tool}" used by ' \
                                'this check is not installed yet'.format(
                                    tool=check.tool.name))
                        else:
                            try:
                                check.run(target, 
                                          arguments,
                                          sqlsession,
                                          fast_mode=fast_mode)

                            except KeyboardInterrupt:
                                print()
                                logger.warning('Check {check} skipped !'.format(
                                    check=check.name))

                    else:
                        logger.info('[{category}][Check {num:02}/{total:02}] ' \
                            '{name} > Skipped because context requirements are ' \
                            'not matching the target'.format(
                                name     = check.name,
                                category = category.capitalize(),
                                num      = j,
                                total    = nb_checks))
                        time.sleep(.2)
                else:

                    logger.info('[{category}][Check {num:02}/{total:02}] ' \
                            '{name} > Skipped because the check has already ' \
                            'been run'.format(
                                name     = check.name,
                                category = category.capitalize(),
                                num      = j,
                                total    = nb_checks))
                    time.sleep(.2)

                i += 1
                j += 1

        checks_progress.update()
        time.sleep(.5)

        checks_progress.close()
        return


    def __run_special_mode(self,
                           target, 
                           arguments,
                           sqlsession,
                           filter_checks=None, 
                           attack_profile=None,
                           fast_mode=False,
                           attack_progress=None):
        """
        Run checks for the service in special mode, i.e. when user has provided
        either an attack profile (pre-selection of checks) or a list of checks
        (may even be one single check to run)

        :param Target target: Target
        :param ArgumentsParser arguments: Arguments from command-line
        :param Session sqlsession: SQLAlchemy session
        :param list filter_checks: Selection of checks to run (default: all)
        :param AttackProfile attack_profile: Attack profile (default: no profile)
        :param bool fast_mode: Set to true to disable prompts
        :param enlighten.Counter attack_progress: Attack progress        
        """

        # User has submitted list of checks
        if filter_checks:
            filter_checks = list(filter(
                lambda x: self.is_existing_check(x), filter_checks))

            if not filter_checks:
                logger.warning('None of the selected checks is existing for the ' \
                    'service {service}'.format(service=target.get_service_name()))
                return

            logger.info('Selected check(s) that will be run:')
            for c in filter_checks:
                check = self.get_check(c)
                if check:
                    Output.print('    | - {name} ({category})'.format(
                        name=c, category=check.category))

        # User has submitted an attack profile
        else:
            if not attack_profile.is_service_supported(target.get_service_name()):
                logger.warning('The attack profile {profile} is not supported for ' \
                    'target service {service}'.format(
                        profile=attack_profile, service=target.get_service_name()))
                return
            else:
                filter_checks = attack_profile.get_checks_for_service(
                    target.get_service_name())

                logger.info('Selected attack profile: {}'.format(attack_profile))


        # Initialize sub status/progress bar
        checks_progress = manager.counter(total=len(filter_checks)+1, 
                                          desc='', 
                                          unit='check',
                                          leave=False,
                                          bar_format=STATUSBAR_FORMAT)
        time.sleep(.5) # hack for progress bar display

        i = 1
        for checkname in filter_checks:
            print()
            check = self.get_check(checkname)

            # Update status/progress bar
            status = ' +--> Current check [{cur}/{total}]: {category} > ' \
                '{checkname}'.format(
                    cur       = i,
                    total     = len(filter_checks),
                    category  = check.category,
                    checkname = checkname)

            checks_progress.desc = '{status}{fill}'.format(
                status = status,
                fill   = ' '*(DESC_LENGTH-len(status)))
            checks_progress.update()
            if attack_progress:
                # Hack to refresh the attack progress bar without incrementing
                # useful if the tool run during the check has cleared the screen
                attack_progress.update(incr=0, force=True) 

            # Run the check if:
            #   - The check has not been already run for this target (except 
            #       if --recheck is specified in command-line)
            #   - Target is compliant with the check,
            #   - The tool used for the check is well installed.

            results_req = ResultsRequester(sqlsession)
            results_req.select_mission(target.service.host.mission.name)
            filter_ = Filter(FilterOperator.AND)
            filter_.add_condition(Condition(target.service.id, 
                FilterData.SERVICE_ID))
            filter_.add_condition(Condition(check.name, FilterData.CHECK_NAME))
            results_req.add_filter(filter_)
            result = results_req.get_first_result()

            if result is None or arguments.args.recheck == True:

                if check.check_target_compliance(target):

                    Output.title2('[Check {num:02}/{total:02}] {name} > ' \
                        '{description}'.format(
                            num         = i,
                            total       = len(filter_checks),
                            name        = check.name,
                            description = check.description))

                    if not check.tool.installed:
                        logger.warning('Skipped: the tool "{tool}" used by ' \
                            'this check is not installed yet'.format(
                                tool=check.tool.name))
                    else:
                        try:
                            check.run(target, 
                                      arguments,
                                      sqlsession,
                                      fast_mode=fast_mode)
                        except KeyboardInterrupt:
                            print()
                            logger.warning('Check {check} skipped !'.format(
                                check=check.name))

                else:
                    logger.info('[Check {num:02}/{total:02}] ' \
                        '{name} > Skipped because context requirements are ' \
                        'not matching the target'.format(
                            name     = check.name,
                            num      = i,
                            total    = len(filter_checks)))
                    time.sleep(.2)

            else:

                logger.info('[Check {num:02}/{total:02}] ' \
                        '{name} > Skipped because the check has already ' \
                        'been run'.format(
                            name     = check.name,
                            num      = i,
                            total    = len(filter_checks)))
                time.sleep(.2)

            i += 1     

        checks_progress.update()
        time.sleep(.5)

        checks_progress.close()  


    #------------------------------------------------------------------------------------
    # Output methods

    def show(self):
        """Display a table with all the checks for the service."""
        data = list()
        columns = [
            'Name',
            'Category',
            'Description',
            'Tool used',
            #'# Commands',
        ]
        for category in self.categories:
            for check in self.checks[category]:
                color_tool = 'grey_19' if not check.tool.installed else None
                data.append([
                    check.name,
                    category,
                    check.description,
                    Output.colored(check.tool.name, color=color_tool),
                    #len(check.commands),
                ])
                
        Output.title1('Checks for service {service}'.format(service=self.service))
        Output.table(columns, data, hrules=False)
Beispiel #7
0
class Toolbox:
    def __init__(self, settings, services):
        """
        Construct the Toolbox object.

        :param Settings settings: Settings from config file
        :param list services: Supported services (including special service "multi")
        """
        self.settings = settings
        self.services = services
        # Organize tools in dict {service: [tools]}
        self.tools = OrderedDefaultDict(list, {k: [] for k in services})

    #------------------------------------------------------------------------------------
    # Dict-like accessors for self.tools

    def __getitem__(self, key):
        return self.tools[key]

    def __setitem__(self, key, value):
        self.tools[key] = value

    def __delitem__(self, key):
        del self.tools[key]

    def __contains__(self, key):
        return key in self.tools

    def __len__(self):
        return len(self.tools)

    def __repr__(self):
        return repr(self.tools)

    def keys(self):
        return self.tools.keys()

    def values(self):
        return self.tools.values()

    #------------------------------------------------------------------------------------
    # Basic Operations

    def add_tool(self, tool):
        """
        Add a tool into the toolbox.

        :param Tool tool: Tool to add
        :return: Status
        :rtype: bool
        """
        if tool.target_service not in self.services:
            return False
        self.tools[tool.target_service].append(tool)
        return True

    def get_tool(self, tool_name):
        """
        Retrieve a tool by its name from toolbox.
        NOT case-sensitive search.

        :param str tool_name: The name of the tool to get
        :return: Tool if found, None otherwise
        :rtype: Tool|None
        """
        for service in self.services:
            for tool in self.tools[service]:
                if tool_name.lower() == tool.name.lower():
                    return tool
        return None

    def nb_tools(self, filter_service=None, only_installed=False):
        """
        Get the number of tools inside the toolbox - installed of not - that target
        either a given service or all services.

        :param str filter_service: Service name to filter with (default: no filter)
        :param bool only_installed: Set to true to count only installed tools
        :return: Number of tools targeting either the given service or all services
        :rtype: int
        """
        if filter_service is not None and filter_service not in self.services:
            return 0

        nb = 0
        services = self.services if filter_service is None else [
            filter_service
        ]
        for service in services:
            for tool in self.tools[service]:
                if only_installed:
                    if tool.installed:
                        nb += 1
                else:
                    nb += 1
        return nb

    #------------------------------------------------------------------------------------
    # Install

    def install_all(self, fast_mode=False):
        """
        Install all tools in the toolbox.

        :param bool fast_mode: Set to true to disable prompts and install checks
        """
        for service in self.services:
            self.install_for_service(service, fast_mode=fast_mode)

    def install_for_service(self, service, fast_mode=False):
        """
        Install the tools for a given service.

        :param str service: Name of the service targeted by the tools to install 
            (may be "multi")
        :param bool fast_mode: Set to true to disable prompts and install checks
        """
        if service not in self.services:
            return

        Output.title1(
            'Install tools for service: {service}'.format(service=service))

        if not self.tools[service]:
            logger.info('No tool specific to this service in the toolbox')
        else:
            i = 1
            for tool in self.tools[service]:
                if i > 1: print()
                Output.title2(
                    '[{svc}][{i:02}/{max:02}] Install {tool_name}:'.format(
                        svc=service,
                        i=i,
                        max=len(self.tools[service]),
                        tool_name=tool.name))

                tool.install(self.settings, fast_mode=fast_mode)
                i += 1

    #------------------------------------------------------------------------------------
    # Update

    def update_all(self, fast_mode=False):
        """
        Update all tools in the toolbox.

        :param bool fast_mode: Set to true to disable prompts and install checks
        """
        for service in self.services:
            self.update_for_service(service, fast_mode=fast_mode)

    def update_for_service(self, service, fast_mode=False):
        """
        Update the tools for a given service.

        :param str service: Name of the service targeted by the tools to update 
            (may be "multi")
        :param bool fast_mode: Set to true to disable prompts and install checks
        """
        if service not in self.services: return
        Output.title1(
            'Update tools for service: {service}'.format(service=service))

        if not self.tools[service]:
            logger.info('No tool specific to this service in the toolbox')
        else:
            i = 1
            for tool in self.tools[service]:
                if i > 1: print()
                Output.title2(
                    '[{svc}][{i:02}/{max:02}] Update {tool_name}:'.format(
                        svc=service,
                        i=i,
                        max=len(self.tools[service]),
                        tool_name=tool.name))

                tool.update(self.settings, fast_mode=fast_mode)
                i += 1

    #------------------------------------------------------------------------------------
    # Remove

    def remove_all(self):
        """Remove all tools in the toolbox."""
        for service in self.services:
            self.remove_for_service(service)

    def remove_for_service(self, service):
        """
        Remove the tools for a given service.

        :param str service: Name of the service targeted by the tools to remove
            (may be "multi")
        """
        if service not in self.services: return
        Output.title1(
            'Remove tools for service: {service}'.format(service=service))

        if not self.tools[service]:
            logger.info('No tool specific to this service in the toolbox')
        else:
            i = 1
            status = True
            for tool in self.tools[service]:
                if i > 1: print()
                Output.title2(
                    '[{svc}][{i:02}/{max:02}] Remove {tool_name}:'.format(
                        svc=service,
                        i=i,
                        max=len(self.tools[service]),
                        tool_name=tool.name))

                status &= tool.remove(self.settings)
                i += 1

            # Remove the service directory if all tools successfully removed
            if status:
                short_svc_path = '{toolbox}/{service}'.format(
                    toolbox=TOOLBOX_DIR, service=service)

                full_svc_path = FileUtils.absolute_path(short_svc_path)

                if FileUtils.remove_directory(full_svc_path):
                    logger.success(
                        'Toolbox service directory "{path}" deleted'.format(
                            path=short_svc_path))
                else:
                    logger.warning('Toolbox service directory "{path}" cannot be ' \
                        'deleted because it still stores some files'.format(
                            path=short_svc_path))

    def remove_tool(self, tool_name):
        """
        Remove one tool from the toolbox.

        :param str tool_name: Name of the tool to remove
        :return: Status of removal
        :rtype: bool
        """
        tool = self.get_tool(tool_name)
        if not tool:
            logger.warning('No tool with this name in the toolbox')
            return False
        else:
            return tool.remove(self.settings)

    #------------------------------------------------------------------------------------
    # Output Methods

    def show_toolbox(self, filter_service=None):
        """
        Display a table showing the content of the toolbox.

        :param str filter_service: Service name to filter with (default: no filter)
        """
        if filter_service is not None and filter_service not in self.services:
            return

        data = list()
        columns = [
            'Name',
            'Service',
            'Status/Update',
            'Description',
        ]

        services = self.services if filter_service is None else [
            filter_service
        ]
        for service in services:
            for tool in self.tools[service]:

                # Install status style
                if tool.installed:
                    status = Output.colored('OK | ' +
                                            tool.last_update.split(' ')[0],
                                            color='green')
                else:
                    status = Output.colored('Not installed', color='red')

                # Add line for the tool
                data.append([
                    tool.name,
                    tool.target_service,
                    status,
                    StringUtils.wrap(tool.description, 120),  # Max line length
                ])

        Output.title1('Toolbox content - {filter}'.format(
            filter='all services' if filter_service is None \
                   else 'service ' + filter_service))

        Output.table(columns, data, hrules=False)
class ServiceChecks:

    def __init__(self, service, categories):
        """
        :param service: Service name
        :param categories: List of categories used to classify the various checks
        """
        self.service    = service
        self.categories = categories
        self.checks     = OrderedDefaultDict(list, {k:[] for k in categories}) # {category: [checks]}


    def add_check(self, check):
        """
        Add a Check
        :param check: Check object
        :return: Boolean indicating status
        """
        self.checks[check.category].append(check)
        return True


    def get_check(self, checkname):
        """
        Get a Check object by name (NOT case-sensitive)
        :param checkname: Name of the check to look for
        :return: Check object
        """
        for cat in self.checks:
            for c in self.checks[cat]:
                if c.name.lower() == checkname.lower():
                    return c
        return None


    def is_existing_check(self, checkname):
        """
        Indicates if a given check name is existing for the current service (NOT case-sensitive)
        :param checkname: Name of the check to look for
        :return: Boolean
        """
        return checkname.lower() in [item.name.lower() for sublist in self.checks.values() for item in sublist]


    def run(self, 
            target, 
            smartmodules_loader, 
            results_requester, 
            filter_categories=None, 
            filter_checks=None, 
            fast_mode=False,
            attack_progress=None):
        """
        Run checks for the service.
        By default, all the categories of checks are runned. Otherwise, only a list of categories
        can be runned.
        :param target: Target object
        :param results_requester: ResultsRequester object
        :param filter_categories: list of categories to run (None for all)
        :param filter_checks: list of checks to run (None for all) 
        """
        categories = self.categories if filter_categories is None else filter_categories

        # Standard mode 
        # Selected/all categories of checks are run
        if filter_checks is None:
            nb_checks = self.nb_checks()

            # Initialize sub status/progress bar
            checks_progress = manager.counter(total=nb_checks+1, 
                                              desc='', 
                                              unit='check',
                                              leave=False,
                                              bar_format=STATUSBAR_FORMAT)
            time.sleep(.5) # hack for progress bar display

            j = 1
            for category in categories:
                Output.title1('Category > {cat}'.format(cat=category.capitalize()))

                i = 1
                for check in self.checks[category]:

                    # Update status/progress bar
                    status = ' +--> Current check [{cur}/{total}]: {category} > {checkname}'.format(
                        cur       = j,
                        total     = nb_checks,
                        category  = check.category,
                        checkname = check.name)
                    checks_progress.desc = '{status}{fill}'.format(
                        status = status,
                        fill   = ' '*(DESC_LENGTH-len(status)))
                    checks_progress.update()
                    if attack_progress:
                        # Hack to refresh the attack progress bar without incrementing
                        # useful if the tool run during the check has cleared the screen
                        attack_progress.refresh()


                    # Run the check if and only if:
                    #   - Check is matching context (i.e. at least one of its command is matching context),
                    #   - The tool used for the check is well installed
                    if i > 1: print()
                    if check.is_matching_context(target):
                        Output.title2('[{category}][Check {num:02}/{total:02}] {name} > {description}'.format(
                            category    = category.capitalize(),
                            num         = i,
                            total       = len(self.checks[category]),
                            name        = check.name,
                            description = check.description))

                        if not check.tool.installed:
                            logger.warning('Skipped: the tool "{tool}" used by this check is not installed yet ' \
                                '(according to config)'.format(tool=check.tool.name_display))
                        else:
                            try:
                                check.run(target, smartmodules_loader, results_requester, fast_mode=fast_mode)
                            except KeyboardInterrupt:
                                print()
                                logger.warning('Check {check} skipped !'.format(check=check.name))

                    else:
                        logger.info('[{category}][Check {num:02}/{total:02}] {name} > Skipped because target\'s context is not matching'.format(
                            name     = check.name,
                            category = category.capitalize(),
                            num      = i,
                            total    = len(self.checks[category])))
                        time.sleep(.2)
                    i += 1
                    j += 1

            checks_progress.update()
            time.sleep(.5)

            checks_progress.close()     

        # Special mode
        # User has provided list of checks to run (may be one single check)
        else:
            filter_checks = list(filter(lambda x: self.is_existing_check(x), filter_checks))
            if not filter_checks:
                logger.warning('None of the selected checks is existing for the service {service}'.format(service=target.get_service_name()))
                return

            # Initialize sub status/progress bar
            checks_progress = manager.counter(total=len(filter_checks)+1, 
                                              desc='', 
                                              unit='check',
                                              leave=False,
                                              bar_format=STATUSBAR_FORMAT)
            time.sleep(.5) # hack for progress bar display

            i = 1
            for checkname in filter_checks:
                print()
                check = self.get_check(checkname)

                # Update status/progress bar
                status = ' +--> Current check [{cur}/{total}]: {category} > {checkname}'.format(
                    cur       = i,
                    total     = len(filter_checks),
                    category  = check.category,
                    checkname = checkname)
                checks_progress.desc = '{status}{fill}'.format(
                    status = status,
                    fill   = ' '*(DESC_LENGTH-len(status)))
                checks_progress.update()
                if attack_progress:
                    # Hack to refresh the attack progress bar without incrementing
                    # useful if the tool run during the check has cleared the screen
                    attack_progress.update(incr=0, force=True) 

                # Run the check
                Output.title2('[Check {num:02}/{total:02}] {name} > {description}'.format(
                        num         = i,
                        total       = len(filter_checks),
                        name        = check.name,
                        description = check.description))
                try:
                    check.run(target, smartmodules_loader, results_requester, fast_mode=fast_mode)
                except KeyboardInterrupt:
                    print()
                    logger.warning('Check {check} skipped !'.format(check=check.name))
                i += 1     

            checks_progress.update()
            time.sleep(.5)

            checks_progress.close()               


    def show(self):
        """
        Show a summary of checks for the service
        :return: None
        """
        data = list()
        columns = [
            'Name',
            'Category',
            'Description',
            'Tool used',
            #'# Commands',
        ]
        for category in self.categories:
            for check in self.checks[category]:
                data.append([
                    check.name,
                    category,
                    check.description,
                    Output.colored(check.tool.name_display, color='grey_19' if not check.tool.installed else None),
                    #len(check.commands),
                ])
        Output.title1('Checks for service {service}'.format(service=self.service))
        Output.table(columns, data, hrules=False)


    def nb_checks(self):
        """
        Get the total number of checks
        :return: Number of checks
        """
        nb = 0
        for category in self.categories:
            nb += len(self.checks[category])
        return nb
Beispiel #9
0
class ServicesConfig:
    """
    ServicesConfig stores all configurations of supported services.

    It is constructed from data parsed from settings files via Settings class.
    """
    def __init__(self, list_services):
        """
        Initialize with list of supported service names

        :param list list_services: List of service names
        """
        self.services = OrderedDefaultDict(
            list,
            {
                k: {
                    'default_port': None,
                    'protocol': None,
                    'specific_options': dict(),  # { specific option : type }
                    'supported_list_options':
                    dict(),  # { specific option : [ values ] }
                    'products': dict(),  # { product type : [ product names ] }
                    'auth_types': None,
                    'checks': None,
                }
                for k in list_services
            })

        #self.services['multi'] = None

    #------------------------------------------------------------------------------------
    # Dict-like accessors for self.services

    def __getitem__(self, key):
        return self.services[key]

    def __setitem__(self, key, value):
        self.services[key] = value

    def __delitem__(self, key):
        del self.services[key]

    def __contains__(self, key):
        return key in self.services

    def __len__(self):
        return len(self.services)

    def __repr__(self):
        return repr(self.services)

    def keys(self):
        return self.services.keys()

    def values(self):
        return self.services.values()

    #------------------------------------------------------------------------------------

    def add_service(self, name, default_port, protocol, specific_options,
                    supported_list_options, products, auth_types,
                    service_checks):
        """
        Add a service configuration

        :param str name: Service name
        :param int default_port: Default port number
        :param str protocol: Protocol tcp or udp
        :param dict specific_options: Supported context-specific options 
            { specific option : type }
        :param dict supported_list_options: Supported values for context-specific options 
            of type "list" { specific option : [ supported values ]  }
        :param dict products: Supported products 
            { product type : [ supported product names ] }
        :param list auth_types: Supported authentication types (relevant for HTTP only)
        :param ServiceChecks service_checks: Checks associated to the service

        :return: None
        """
        service = name.lower()
        self.services[service]['default_port'] = int(default_port)
        self.services[service]['protocol'] = protocol
        self.services[service]['specific_options'] = specific_options
        self.services[service][
            'supported_list_options'] = supported_list_options
        self.services[service]['products'] = products
        self.services[service]['auth_types'] = auth_types
        self.services[service]['checks'] = service_checks

    #------------------------------------------------------------------------------------
    # Getters

    def list_services(self, multi=False):
        """
        List all services

        :param bool multi: Indicates if "multi" must be added in resulting list
        :return: Supported service names
        :rtype: list
        """
        if multi:
            return sorted(list(self.services.keys()))
        else:
            l = list(self.services.keys())
            l.remove('multi')
            return sorted(l)

    def list_all_categories(self):
        """
        List all categories of checks supported accross all services

        :return: Names of categories of checks
        :rtype: set
        """
        categories = set()
        for svc in self.list_services():
            categories.update(self.services[svc]['checks'].categories)
        return categories

    def get_default_port(self, service):
        if not self.is_service_supported(service, multi=False):
            return None
        return self.services[service]['default_port']

    def get_protocol(self, service):
        if not self.is_service_supported(service, multi=False):
            return None
        return self.services[service]['protocol']

    def get_protocol2(self, service):
        if not self.is_service_supported(service, multi=False):
            return None
        return {
            'tcp': Protocol.TCP,
            'udp': Protocol.UDP
        }.get(self.get_protocol(service))

    def get_authentication_types(self, service='http'):
        if not self.is_service_supported(service, multi=False):
            return []
        return self.services[service]['auth_types']

    def get_service_checks(self, service):
        if not self.is_service_supported(service, multi=False):
            return None
        return self.services[service]['checks']

    def get_service_from_port(self, port, protocol='tcp'):
        """
        Try to get the service name from the port number.
        Based on default port numbers.

        :param int port: Port number
        :param str protocol: Protocol (default: tcp)
        :return: Service name if found, None otherwise
        :rtype: str|None
        """
        for service in self.list_services():
            if self.get_default_port(service) == port \
               and self.get_protocol(service) == protocol:
                return service
        return None

    #------------------------------------------------------------------------------------
    # Services/Security checks existence checkers

    def is_service_supported(self, service, multi=True):
        """
        Check if given service is supported.
        NOT case-sensitive.


        :param service: Service name to check
        :param multi: If set to True, "multi" should be considered as valid service name
        :return: Result
        :rtype: bool
        """
        return service.lower() in self.list_services(multi)

    def is_existing_check(self, check_name):
        """
        Check if a given check name is existing for any supported service. 
        NOT case-sensitive.

        :param check_name: Name of the check to look for
        :return: Result
        :rtype: bool
        """
        for svc in self.list_services():
            if self.services[svc]['checks'].is_existing_check(check_name):
                return True
        return False

    #------------------------------------------------------------------------------------
    # Authentication type checker

    def is_valid_auth_type(self, auth_type):
        """
        Check if a given authentication type is valid.
        Relevant only for HTTP.

        :param str auth_type: Authentication type
        :return: Result
        :rtype: bool
        """
        return auth_type.lower() in self.get_authentication_types('http')

    #------------------------------------------------------------------------------------
    # Specific-options checkers/accessors

    def is_specific_option_name_supported(self, option, service=None):
        """
        Check if a given context-specific option name is valid, either for any service
        or for a given service

        :param option: Context-specific option name to check
        :param service: Service name or None if check for any service
        :return: Result
        :rtype: bool
        """
        if service is not None and not self.is_service_supported(service,
                                                                 multi=False):
            return False

        services = self.list_services() if service is None else [service]
        for service in services:
            if option in self.services[service]['specific_options'].keys():
                return True
        return False

    def is_specific_option_value_supported(self, name, value):
        """
        Check if the value for a given context-specific option is valid

        :param name: Context-specific option name
        :param value: Context-specific option value
        :return: Boolean
        """
        service = self.get_service_for_specific_option(name)
        if service:
            type_ = self.services[service]['specific_options'][name]
            if type_ == OptionType.BOOLEAN:
                return value in ('true', 'false')
            elif type_ == OptionType.LIST:
                return value in self.services[service][
                    'supported_list_options'][name]
            else:
                # For option of type "var", value is free
                return True
        return False

    def get_specific_option_type(self, option, service):
        """
        Get the type of a context-specific option

        :param option: Context-specific option name
        :param service: Service name
        :return: OptionType
        """
        if self.is_specific_option_name_supported(option, service):
            return self.services[service]['specific_options'][option]
        else:
            return None

    def get_service_for_specific_option(self, name):
        """
        Get the service name on which a specific option is applied
        :param name: Context-specific option name
        :return: Service name or None if not found
        """
        for service in self.list_services():
            if name in self.services[service]['specific_options'].keys():
                return service
        return None

    #------------------------------------------------------------------------------------
    # Products checkers/accessors

    def is_product_type_supported(self, product_type, service=None):
        """
        Check if a given product type is valid, either for any service or for a given 
        service.

        :param str product_type: Product type to check
        :param str service: Service name or None if check for any service
        :return: Result
        :rtype: bool
        """
        if service is not None and not self.is_service_supported(service,
                                                                 multi=False):
            return False

        services = self.list_services() if service is None else [service]

        # print(self.services)
        for service in services:
            # print('--{}--'.format(self.services[service]['products']))
            # print('--{}--'.format(self.services[service]['products'].keys()))
            if product_type in self.services[service]['products'].keys():
                return True
        return False

    def is_product_name_supported(self, product_type, product_name):
        """
        Check if a product name associated to a given type is valid.
        Case insensitive lookup.
        (e.g. product_type=web_server, product_name=Apache)

        :param str product_type: Product type
        :param str product_name: Product name to check
        :return: Result
        :rtype: bool
        """
        service = self.get_service_for_product_type(product_type)
        if service:
            return product_name.lower() in list(
                map(lambda x: x.lower(),
                    self.services[service]['products'][product_type]))
        return False

    def get_service_for_product_type(self, product_type):
        """
        Get the service name for which a product type is used
        (e.g. for product_type=web_server, result is 'http')

        :param str product_type: Product type to look for
        :return: Service name or None if not found
        :rtype: str|None
        """
        for service in self.list_services():
            if product_type in self.services[service]['products'].keys():
                return service
        return None

    #------------------------------------------------------------------------------------
    # Output methods

    def show_services(self, toolbox):
        """
        Display supported services in a table.

        :param Toolbox toolbox: Toolbox
        """
        data = list()
        columns = [
            'Service',
            'Default port',
            '# Tools',
            '# Checks',
        ]
        for service in self.list_services(multi=True):
            data.append([
                service,
                'N/A' if service == 'multi' else '{port}/{proto}'.format(
                    port  = self.services[service]['default_port'],
                    proto = self.services[service]['protocol']),
                '{nb_installed}/{nb_tools}'.format(
                    nb_installed = toolbox.nb_tools(filter_service=service,
                                                    only_installed=True),
                    nb_tools     = toolbox.nb_tools(filter_service=service)),
                'N/A' if service == 'multi' \
                      else self.services[service]['checks'].nb_checks(),
            ])

        Output.title1('Supported services')
        Output.table(columns, data, hrules=False)

    def show_categories(self, filter_service=None):
        """
        Show list of categories of checks for the given service or all services
        :param filter_service: None or given service
        :return: None
        """
        data = list()
        columns = [
            'Category',
            'Services',
        ]
        services = self.list_services() if filter_service is None else [
            filter_service
        ]
        svcbycat = defaultdict(list)
        for service in services:
            for category in self.services[service]['checks'].categories:
                svcbycat[category].append(service)

        for category in svcbycat:
            data.append([
                category,
                StringUtils.wrap(', '.join(svcbycat[category]), 100)
            ])

        Output.table(columns, data)

    def show_specific_options(self, filter_service=None):
        """
        Display supported specific options in a table.

        :param list filter_service: Filter on services (default: all)
        """
        data = list()
        columns = [
            'Option',
            'Service',
            'Supported values',
        ]
        services = self.list_services() if filter_service is None else [
            filter_service
        ]
        for service in services:
            options = self.services[service]['specific_options']
            for opt in options:
                if options[opt] == OptionType.BOOLEAN:
                    values = 'true, false'

                elif options[opt] == OptionType.LIST:
                    values = sorted(
                        self.services[service]['supported_list_options'][opt])
                    values = StringUtils.wrap(', '.join(values), 80)

                else:
                    values = '<anything>'
                data.append([opt, service, values])

        Output.title1('Available context-specific options for {filter}'.format(
            filter='all services' if filter_service is None \
                   else 'service ' + filter_service))

        if not data:
            logger.warning('No specific option')
        else:
            Output.table(columns, data, hrules=False)

    def show_products(self, filter_service=None):
        """
        Display supported products in a table

        :param list filter_service: Filter on services (default: all)
        """
        data = list()
        columns = [
            'Type',
            'Product Names',
        ]
        services = self.list_services() if filter_service is None else [
            filter_service
        ]
        for service in services:
            products = self.services[service]['products']
            for product_type in products:
                names = sorted(
                    self.services[service]['products'][product_type])
                names = StringUtils.wrap(', '.join(names), 100)

                data.append([product_type, names])

        Output.title1('Available products for {filter}'.format(
            filter='all services' if filter_service is None \
                   else 'service ' + filter_service))

        if not data:
            logger.warning('No product')
        else:
            Output.table(columns, data)

    def show_authentication_types(self, service='http'):
        """Display list of authentication types for HTTP."""
        Output.title1('Supported {service} authentication types'.format(
            service=service.upper()))

        if not self.is_service_supported(service, multi=False):
            logger.warning('The service {service} is not supported'.format(
                service=service))

        elif not self.services[service]['auth_types']:
            logger.warning('No special authentication type for this service')

        else:
            data = list()
            for t in sorted(self.services[service]['auth_types']):
                data.append([t])
            Output.table(['Authentication types'], data, hrules=False)
Beispiel #10
0
class Toolbox:
    def __init__(self, settings, services):
        """
        Construct the Toolbox object.

        :param Settings settings: Settings from config file
        :param list services: Supported services (including special service "multi")
        """
        self.settings = settings
        self.services = services
        # Organize tools in dict {service: [tools]}
        self.tools = OrderedDefaultDict(list, {k: [] for k in services})

    #------------------------------------------------------------------------------------
    # Dict-like accessors for self.tools

    def __getitem__(self, key):
        return self.tools[key]

    def __setitem__(self, key, value):
        self.tools[key] = value

    def __delitem__(self, key):
        del self.tools[key]

    def __contains__(self, key):
        return key in self.tools

    def __len__(self):
        return len(self.tools)

    def __repr__(self):
        return repr(self.tools)

    def keys(self):
        return self.tools.keys()

    def values(self):
        return self.tools.values()

    #------------------------------------------------------------------------------------
    # Basic Operations

    def add_tool(self, tool):
        """
        Add a tool into the toolbox.

        :param Tool tool: Tool to add
        :return: Status
        :rtype: bool
        """
        if tool.target_service not in self.services:
            return False
        self.tools[tool.target_service].append(tool)
        return True

    def get_tool(self, tool_name):
        """
        Retrieve a tool by its name from toolbox.
        NOT case-sensitive search.

        :param str tool_name: The name of the tool to get
        :return: Tool if found, None otherwise
        :rtype: Tool|None
        """
        for service in self.services:
            for tool in self.tools[service]:
                if tool_name.lower() == tool.name.lower():
                    return tool
        return None

    def nb_tools(self, filter_service=None, only_installed=False):
        """
        Get the number of tools inside the toolbox - installed of not - that target
        either a given service or all services.

        :param str filter_service: Service name to filter with (default: no filter)
        :param bool only_installed: Set to true to count only installed tools
        :return: Number of tools targeting either the given service or all services
        :rtype: int
        """
        if filter_service is not None and filter_service not in self.services:
            return 0

        nb = 0
        services = self.services if filter_service is None else [
            filter_service
        ]
        for service in services:
            for tool in self.tools[service]:
                if only_installed:
                    if tool.installed:
                        nb += 1
                else:
                    nb += 1
        return nb

    #------------------------------------------------------------------------------------
    # Install

    def install_all(self, fast_mode=False):
        """
        Install all tools in the toolbox.

        :param bool fast_mode: Set to true to disable prompts and install checks
        """
        for service in self.services:
            self.install_for_service(service, fast_mode=fast_mode)

    def install_for_service(self, service, fast_mode=False):
        """
        Install the tools for a given service.

        :param str service: Name of the service targeted by the tools to install 
            (may be "multi")
        :param bool fast_mode: Set to true to disable prompts and install checks
        """
        if service not in self.services:
            return

        Output.title1(
            'Install tools for service: {service}'.format(service=service))

        if not self.tools[service]:
            logger.info('No tool specific to this service in the toolbox')
        else:
            i = 1
            for tool in self.tools[service]:
                if i > 1: print()
                Output.title2(
                    '[{svc}][{i:02}/{max:02}] Install {tool_name}:'.format(
                        svc=service,
                        i=i,
                        max=len(self.tools[service]),
                        tool_name=tool.name))

                tool.install(self.settings, fast_mode=fast_mode)
                i += 1

    def install_tool(self, tool_name, fast_mode=False):
        """
        Install one tool from the toolbox.

        :param str tool_name: Name of the tool to install
        :param bool fast_mode: Set to true to disable prompts and install checks
        :return: Status of install
        :rtype: bool
        """
        tool = self.get_tool(tool_name)
        if not tool:
            logger.warning('No tool with this name in the toolbox')
            return False
        else:
            Output.title2('Install {tool_name}:'.format(tool_name=tool.name))
            return tool.install(self.settings, fast_mode)

    #------------------------------------------------------------------------------------
    # Update

    def update_all(self, fast_mode=False):
        """
        Update all tools in the toolbox.

        :param bool fast_mode: Set to true to disable prompts and install checks
        """
        for service in self.services:
            self.update_for_service(service, fast_mode=fast_mode)

    def update_for_service(self, service, fast_mode=False):
        """
        Update the tools for a given service.

        :param str service: Name of the service targeted by the tools to update 
            (may be "multi")
        :param bool fast_mode: Set to true to disable prompts and install checks
        """
        if service not in self.services: return
        Output.title1(
            'Update tools for service: {service}'.format(service=service))

        if not self.tools[service]:
            logger.info('No tool specific to this service in the toolbox')
        else:
            i = 1
            for tool in self.tools[service]:
                if i > 1: print()
                Output.title2(
                    '[{svc}][{i:02}/{max:02}] Update {tool_name}:'.format(
                        svc=service,
                        i=i,
                        max=len(self.tools[service]),
                        tool_name=tool.name))

                tool.update(self.settings, fast_mode=fast_mode)
                i += 1

    def update_tool(self, tool_name, fast_mode=False):
        """
        Update one tool from the toolbox.

        :param str tool_name: Name of the tool to update
        :param bool fast_mode: Set to true to disable prompts and install checks
        :return: Status of update
        :rtype: bool
        """
        tool = self.get_tool(tool_name)
        if not tool:
            logger.warning('No tool with this name in the toolbox')
            return False
        else:
            Output.title2('Update {tool_name}:'.format(tool_name=tool.name))
            return tool.update(self.settings, fast_mode)

    #------------------------------------------------------------------------------------
    # Remove

    def remove_all(self):
        """Remove all tools in the toolbox."""
        for service in self.services:
            self.remove_for_service(service)

    def remove_for_service(self, service):
        """
        Remove the tools for a given service.

        :param str service: Name of the service targeted by the tools to remove
            (may be "multi")
        """
        if service not in self.services: return
        Output.title1(
            'Remove tools for service: {service}'.format(service=service))

        if not self.tools[service]:
            logger.info('No tool specific to this service in the toolbox')
        else:
            i = 1
            status = True
            for tool in self.tools[service]:
                if i > 1: print()
                Output.title2(
                    '[{svc}][{i:02}/{max:02}] Remove {tool_name}:'.format(
                        svc=service,
                        i=i,
                        max=len(self.tools[service]),
                        tool_name=tool.name))

                status &= tool.remove(self.settings)
                i += 1

            # Remove the service directory if all tools successfully removed
            if status:
                short_svc_path = '{toolbox}/{service}'.format(
                    toolbox=TOOLBOX_DIR, service=service)

                full_svc_path = FileUtils.absolute_path(short_svc_path)

                if FileUtils.remove_directory(full_svc_path):
                    logger.success(
                        'Toolbox service directory "{path}" deleted'.format(
                            path=short_svc_path))
                else:
                    logger.warning('Toolbox service directory "{path}" cannot be ' \
                        'deleted because it still stores some files'.format(
                            path=short_svc_path))

    def remove_tool(self, tool_name):
        """
        Remove one tool from the toolbox.

        :param str tool_name: Name of the tool to remove
        :return: Status of removal
        :rtype: bool
        """
        tool = self.get_tool(tool_name)
        if not tool:
            logger.warning('No tool with this name in the toolbox')
            return False
        else:
            Output.title2('Remove {tool_name}:'.format(tool_name=tool.name))
            return tool.remove(self.settings)

    #------------------------------------------------------------------------------------
    # Check

    def check(self):
        """
        Check the toolbox: Run all check commands (when available) from all
        installed tools, in automatic mode (i.e. checks based on exit codes)

        In case of an error code returned by one check command (!= 0), the function
        stops and exits the program with exit code 1 (error). 
        Otherwise, if all check commands have returned a success exit code (0), 
        it exits the program with exit code 0 (success).

        Designed to be used for Continuous Integration.
        """
        Output.title1('Automatic check of installed tools')
        for service in self.services:
            for tool in self.tools[service]:
                if tool.installed:
                    # Automatic mode (no prompt), only based on exit status
                    status = tool.run_check_command(fast_mode=True)
                    if not status:
                        logger.error('An error occured with the tool "{tool}". Exit ' \
                            'check with exit code 1...'.format(tool=tool.name))
                        sys.exit(1)
                print()
                print()

        logger.success('No error has been detected with all tools check commands. ' \
            'Exit with success code 0...')
        sys.exit(0)

    #------------------------------------------------------------------------------------
    # Output Methods

    def show_toolbox(self, filter_service=None):
        """
        Display a table showing the content of the toolbox.

        :param str filter_service: Service name to filter with (default: no filter)
        """
        if filter_service is not None and filter_service not in self.services:
            return

        data = list()
        columns = [
            'Name',
            'Service',
            'Status/Update',
            'Description',
        ]

        services = self.services if filter_service is None else [
            filter_service
        ]
        for service in services:
            for tool in self.tools[service]:

                # Install status style
                if tool.installed:
                    status = Output.colored('OK | ' +
                                            tool.last_update.split(' ')[0],
                                            color='green')
                else:
                    status = Output.colored('Not installed', color='red')

                # Add line for the tool
                data.append([
                    tool.name,
                    tool.target_service,
                    status,
                    StringUtils.wrap(tool.description, 120),  # Max line length
                ])

        Output.title1('Toolbox content - {filter}'.format(
            filter='all services' if filter_service is None \
                   else 'service ' + filter_service))

        Output.table(columns, data, hrules=False)

    #------------------------------------------------------------------------------------
    # Compare Toolbox objects

    def compare_with_new(self, toolbox_new):
        """

        :return: new tools, tools with updated config, removed tools
        :rtype: { 'new': list(str), 'updated': list(str), 'deleted': list(str) }
        """
        results = {
            'new': list(),
            'updated': list(),
            'deleted': list(),
        }

        toolbox_new_toolnames = list()
        for s in toolbox_new.tools.keys():
            for tool_new in toolbox_new.tools[s]:
                tool_bak = self.get_tool(tool_new.name)
                toolbox_new_toolnames.append(tool_new.name)

                # New tool
                if tool_bak is None:
                    results['new'].append(tool_new.name)

                # Updated tool
                elif tool_bak.target_service != tool_new.target_service \
                     or tool_bak.install_command != tool_new.install_command \
                     or tool_bak.update_commmand != tool_new.update_commmand:
                    results['updated'].append(tool_new.name)

        # Look for deleted tools
        for s in self.tools.keys():
            for tool_bak in self.tools[s]:
                if tool_bak.name not in toolbox_new_toolnames:
                    results['deleted'].append(tool_bak.name)

        return results
Beispiel #11
0
class ServicesConfig:
    """
    Map each supported service to their settings and to their ServiceChecks object
    """
    def __init__(self, list_services):
        """
        Initialize ServicesConfig with a list of service names
        :param list_services: List of service names
        """
        self.services = OrderedDefaultDict(
            list,
            {
                k: {
                    'default_port': None,
                    'protocol': None,
                    'specific_options':
                    dict(),  # Dictionary { specific option : type }
                    'supported_list_options': dict(
                    ),  # Dictionary { specific option : list of possible values }
                    'auth_types': None,
                    'checks': None,
                }
                for k in list_services
            })

        #self.services['multi'] = None

    def __getitem__(self, key):
        return self.services[key]

    def __setitem__(self, key, value):
        self.services[key] = value

    def __delitem__(self, key):
        del self.services[key]

    def __contains__(self, key):
        return key in self.services

    def __len__(self):
        return len(self.services)

    def __repr__(self):
        return repr(self.services)

    def keys(self):
        return self.services.keys()

    def values(self):
        return self.services.values()

    def add_service(self, name, default_port, protocol, specific_options,
                    supported_list_options, auth_types, service_checks):
        """
        Add a service configuration
        :param name: Service name
        :param default_port: Default port number
        :param protocol: Protocol tcp or udp
        :param specific_options: Dictionary { specific option : type }
        :param supported_list_options: Dictionary { specific option : list of possible values }
        :param auth_types: List of supported authentication types, relevant for HTTP only
        :param service_checks: ServiceChecks object
        """
        service = name.lower()
        self.services[service]['default_port'] = int(default_port)
        self.services[service]['protocol'] = protocol
        self.services[service]['specific_options'] = specific_options
        self.services[service][
            'supported_list_options'] = supported_list_options
        self.services[service]['auth_types'] = auth_types
        self.services[service]['checks'] = service_checks

    def list_services(self, multi=False):
        """
        :return: List of service names
        """
        if multi:
            return sorted(list(self.services.keys()))
        else:
            l = list(self.services.keys())
            l.remove('multi')
            return sorted(l)

    def list_all_categories(self):
        """
        :return: Set of all categories of checks (for all services)
        """
        categories = set()
        for svc in self.list_services():
            categories.update(self.services[svc]['checks'].categories)
        return categories

    def get_default_port(self, service):
        if not self.is_service_supported(service, multi=False):
            return None
        return self.services[service]['default_port']

    def get_protocol(self, service):
        if not self.is_service_supported(service, multi=False):
            return None
        return self.services[service]['protocol']

    def get_authentication_types(self, service='http'):
        if not self.is_service_supported(service, multi=False):
            return []
        return self.services[service]['auth_types']

    def get_service_checks(self, service):
        if not self.is_service_supported(service, multi=False):
            return None
        return self.services[service]['checks']

    def get_service_from_port(self, port, protocol='tcp'):
        """
        Try to get the service name from the port number.
        Research is based on default ports
        :param port: Port number
        :param protocol: By default TCP
        :return: Service name if found, None otherwise
        """
        for service in self.list_services():
            if self.get_default_port(service) == port and self.get_protocol(
                    service) == protocol:
                return service
        return None

    def is_service_supported(self, service, multi=True):
        """
        Check if given service is supported
        :param service: Service name to check
        :param multi: Boolean indicating if "multi" should be considered as valid service name
        """
        return service.lower() in self.list_services(multi)

    def is_existing_check(self, check_name):
        """
        Indicates if a given check name is existing for any supported service (NOT case-sensitive)
        :param check_name: Name of the check to look for
        :return: Boolean
        """
        for svc in self.list_services():
            if self.services[svc]['checks'].is_existing_check(check_name):
                return True
        return False

    def is_valid_authentication_type(self, auth_type, service='http'):
        return auth_type.lower() in self.get_authentication_types(service)

    def is_specific_option_name_supported(self, option, service=None):
        """
        Check if a given context-specific option name is valid, either for any service
        or for a given service
        :param option: Context-specific option name to check
        :param service: Service name or None if check for any service
        :return: Boolean
        """
        if service is not None and not self.is_service_supported(service,
                                                                 multi=False):
            return False

        services = self.list_services() if service is None else [service]
        for service in services:
            if option in self.services[service]['specific_options'].keys():
                return True
        return False

    def is_specific_option_value_supported(self, name, value):
        """
        Check if the value for a given context-specific option is valid
        :param name: Context-specific option name
        :param value: Context-specific option value
        :return: Boolean
        """
        for service in self.list_services():
            if name in self.services[service]['specific_options'].keys():
                type_ = self.services[service]['specific_options'][name]
                if type_ == OptionType.BOOLEAN:
                    return value in ('true', 'false')
                elif type_ == OptionType.LIST:
                    return value not in self.services[service][
                        'supported_list_options'][name]
                else:
                    return True
        return False

    def get_specific_option_type(self, option, service):
        """
        Get the type of a context-specific option
        :param option: Context-specific option name
        :param service: Service name
        :return: OptionType
        """
        if self.is_specific_option_name_supported(option, service):
            return self.services[service]['specific_options'][option]
        else:
            return None

    def get_service_for_specific_option(self, name):
        """
        Get the service name on which a specific option is applied
        :param name: Context-specific option name
        :return: Service name or None if not found
        """
        for service in self.list_services():
            if name in self.services[service]['specific_options'].keys():
                return service
        return None

    def show_services(self, toolbox):
        """
        Show all supported services along with number of installed tools / total number
        :param toolbox: Toolbox object
        :return: None
        """
        data = list()
        columns = [
            'Service',
            'Default port',
            '# Tools',
            '# Checks',
        ]
        for service in self.list_services(multi=True):
            data.append([
                service,
                'N/A' if service == 'multi' else '{port}/{proto}'.format(
                    port=self.services[service]['default_port'],
                    proto=self.services[service]['protocol']),
                '{nb_installed}/{nb_tools}'.format(
                    nb_installed=toolbox.nb_tools_installed(service),
                    nb_tools=toolbox.nb_tools(service)),
                'N/A' if service == 'multi' else
                self.services[service]['checks'].nb_checks(),
            ])

        Output.title1('Supported services')
        Output.table(columns, data, hrules=False)

    def show_specific_options(self, filter_service=None):
        """
        Show list of available specific option for the given service or all services
        :param filter_service: None or given service
        :return: None
        """
        data = list()
        columns = [
            'Option',
            'Service',
            'Supported values',
        ]
        services = self.list_services() if filter_service is None else [
            filter_service
        ]
        for service in services:
            options = self.services[service]['specific_options']
            for opt in options:
                if options[opt] == OptionType.BOOLEAN:
                    values = 'true, false'
                elif options[opt] == OptionType.LIST:
                    values = sorted(
                        self.services[service]['supported_list_options'][opt])
                    values = StringUtils.wrap(', '.join(values), 80)
                else:
                    values = '<anything>'
                data.append([opt, service, values])

        Output.title1('Available context-specific options for {filter}'.format(
            filter='all services' if filter_service is None else 'service ' +
            filter_service))

        if not data:
            logger.warning('No specific option')
        else:
            Output.table(columns, data)

    def show_authentication_types(self, service):
        """
        Show list of authentication types for the given service.
        Actually relevant only for HTTP (for now ?)
        :param service: Service name
        :return: None
        """
        Output.title1('Supported {service} authentication types'.format(
            service=service.upper()))
        if not self.is_service_supported(service, multi=False):
            logger.warning('The service {service} is not supported'.format(
                service=service))
        elif not self.services[service]['auth_types']:
            logger.warning('No special authentication type for this service')
        else:
            data = list()
            for t in sorted(self.services[service]['auth_types']):
                data.append([t])
            Output.table(['Authentication types'], data, hrules=False)

    def show_categories(self, filter_service=None):
        """
        Show list of categories of checks for the given service or all services
        :param filter_service: None or given service
        :return: None
        """
        data = list()
        columns = [
            'Category',
            'Services',
        ]
        services = self.list_services() if filter_service is None else [
            filter_service
        ]
        svcbycat = defaultdict(list)
        for service in services:
            for category in self.services[service]['checks'].categories:
                svcbycat[category].append(service)

        for category in svcbycat:
            data.append([
                category,
                StringUtils.wrap(', '.join(svcbycat[category]), 100)
            ])

        Output.table(columns, data)