Exemple #1
0
def test_read_wrong_configuration(mock_exists, config):
    """Verify that expected exceptions are raised when incorrect configuration"""
    with patch('api.configuration.yaml.safe_load') as m:
        with pytest.raises(api_exception.APIError, match=r'\b2004\b'):
            configuration.read_yaml_config()

        with patch('builtins.open'):
            m.return_value = config
            with pytest.raises(api_exception.APIError, match=r'\b2000\b'):
                configuration.read_yaml_config()
def test_read_wrong_configuration(mock_exists):
    """Verify that expected exceptions are raised when incorrect configuration"""
    with patch('api.configuration.yaml.safe_load') as m:
        with pytest.raises(api_exception.APIException, match='.* 2004 .*'):
            configuration.read_yaml_config()

        with patch('builtins.open'):
            m.return_value = {'marta': 'yay'}
            with pytest.raises(api_exception.APIException, match='.* 2000 .*'):
                configuration.read_yaml_config()
def test_config(config_file):
    """Make an attempt to read the API config file. Exits with 0 code if successful, 1 otherwise.

    Arguments
    ---------
    config_file : str
        Path of the file
    """
    try:
        configuration.read_yaml_config(config_file=config_file)
    except Exception as e:
        print(f"Configuration not valid: {e}")
        sys.exit(1)
    sys.exit(0)
Exemple #4
0
def test_config(config_file: str):
    """Make an attempt to read the API config file. Exits with 0 code if successful, 1 otherwise.

    Arguments
    ---------
    config_file : str
        Path of the file
    """
    try:
        from api.configuration import read_yaml_config
        read_yaml_config(config_file=config_file)
    except Exception as exc:
        print(f"Configuration not valid. ERROR: {exc}")
        sys.exit(1)
    sys.exit(0)
Exemple #5
0
def test_read_configuration(mock_open, mock_exists, read_config):
    """ Tests reading different API configurations."""
    with patch('api.configuration.yaml.safe_load') as m:
        m.return_value = copy.deepcopy(read_config)
        config = configuration.read_yaml_config()
        for section, subsection in [('logs', 'path'), ('https', 'key'), ('https', 'cert'), ('https', 'ca')]:
            config[section][subsection] = config[section][subsection].replace(common.ossec_path+'/', '')

        check_config_values(config, {}, read_config)

        # values not present in the read user configuration will be filled with default values
        check_config_values(config, read_config, configuration.default_api_configuration)
Exemple #6
0
def test_read_configuration(mock_open, mock_exists, read_config):
    """ Tests reading different API configurations."""
    with patch('api.configuration.yaml.safe_load') as m:
        m.return_value = copy.deepcopy(read_config)
        config = configuration.read_yaml_config()
        # Currently we only add SSL path to HTTPS options
        for section, subsection in [('https', 'key'), ('https', 'cert'),
                                    ('https', 'ca')]:
            config[section][subsection] = config[section][subsection].replace(
                f'{api.constants.API_SSL_PATH}/', '')

        check_config_values(config, {}, read_config)

        # values not present in the read user configuration will be filled with default values
        check_config_values(config, read_config,
                            configuration.default_api_configuration)
def start(foreground, root, config_file):
    """Run the Wazuh API.

    If another Wazuh API is running, this function fails.
    This function exits with 0 if successful or 1 if failed because the API was already running.

    Arguments
    ---------
    foreground : bool
        If the API must be daemonized or not
    root : bool
        If true, the daemon is run as root. Normally not recommended for security reasons
    config_file : str
        Path to the API config file
    """
    import asyncio
    import logging
    import os
    import ssl

    import connexion
    import uvloop
    from aiohttp_cache import setup_cache

    import wazuh.security
    from api import __path__ as api_path
    # noinspection PyUnresolvedReferences
    from api import validator
    from api.api_exception import APIError
    from api.constants import CONFIG_FILE_PATH
    from api.middlewares import set_user_name, security_middleware, response_postprocessing, request_logging, \
        set_secure_headers
    from api.uri_parser import APIUriParser
    from api.util import to_relative_path
    from wazuh.core import pyDaemonModule

    configuration.api_conf.update(
        configuration.read_yaml_config(config_file=config_file))
    api_conf = configuration.api_conf
    security_conf = configuration.security_conf
    log_path = api_conf['logs']['path']

    # Set up logger
    set_logging(log_path=log_path,
                debug_mode=api_conf['logs']['level'],
                foreground_mode=foreground)
    logger = logging.getLogger('wazuh-api')

    # Set correct permissions on api.log file
    if os.path.exists(os.path.join(common.wazuh_path, log_path)):
        os.chown(os.path.join(common.wazuh_path, log_path), common.wazuh_uid(),
                 common.wazuh_gid())
        os.chmod(os.path.join(common.wazuh_path, log_path), 0o660)

    # Configure https
    ssl_context = None
    if api_conf['https']['enabled']:
        try:
            # Generate SSL if it does not exist and HTTPS is enabled
            if not os.path.exists(
                    api_conf['https']['key']) or not os.path.exists(
                        api_conf['https']['cert']):
                logger.info(
                    'HTTPS is enabled but cannot find the private key and/or certificate. '
                    'Attempting to generate them')
                private_key = configuration.generate_private_key(
                    api_conf['https']['key'])
                logger.info(
                    f"Generated private key file in WAZUH_PATH/{to_relative_path(api_conf['https']['key'])}"
                )
                configuration.generate_self_signed_certificate(
                    private_key, api_conf['https']['cert'])
                logger.info(
                    f"Generated certificate file in WAZUH_PATH/{to_relative_path(api_conf['https']['cert'])}"
                )

            # Load SSL context
            allowed_ssl_ciphers = {
                'tls': ssl.PROTOCOL_TLS,
                'tlsv1': ssl.PROTOCOL_TLSv1,
                'tlsv1.1': ssl.PROTOCOL_TLSv1_1,
                'tlsv1.2': ssl.PROTOCOL_TLSv1_2
            }
            try:
                ssl_cipher = allowed_ssl_ciphers[api_conf['https']
                                                 ['ssl_cipher'].lower()]
            except (KeyError, AttributeError):
                # KeyError: invalid string value
                # AttributeError: invalid boolean value
                logger.error(
                    str(
                        APIError(
                            2003,
                            details='SSL cipher is not valid. Allowed values: '
                            'TLS, TLSv1, TLSv1.1, TLSv1.2')))
                sys.exit(1)
            ssl_context = ssl.SSLContext(protocol=ssl_cipher)
            if api_conf['https']['use_ca']:
                ssl_context.verify_mode = ssl.CERT_REQUIRED
                ssl_context.load_verify_locations(api_conf['https']['ca'])
            ssl_context.load_cert_chain(certfile=api_conf['https']['cert'],
                                        keyfile=api_conf['https']['key'])
        except ssl.SSLError:
            logger.error(
                str(
                    APIError(
                        2003,
                        details=
                        'Private key does not match with the certificate')))
            sys.exit(1)
        except IOError as e:
            if e.errno == 22:
                logger.error(
                    str(APIError(2003, details='PEM phrase is not correct')))
            elif e.errno == 13:
                logger.error(
                    str(
                        APIError(
                            2003,
                            details=
                            'Ensure the certificates have the correct permissions'
                        )))
            else:
                print(
                    'Wazuh API SSL ERROR. Please, ensure if path to certificates is correct in the configuration '
                    f'file WAZUH_PATH/{to_relative_path(CONFIG_FILE_PATH)}')
            sys.exit(1)

    # Drop privileges to wazuh
    if not root:
        if api_conf['drop_privileges']:
            os.setgid(common.wazuh_gid())
            os.setuid(common.wazuh_uid())
    else:
        print(f"Starting API as root")

    # Foreground/Daemon
    if not foreground:
        pyDaemonModule.pyDaemon()
        pyDaemonModule.create_pid('wazuh-apid', os.getpid())
    else:
        print(f"Starting API in foreground")

    # Load the SPEC file into memory to use as a reference for future calls
    wazuh.security.load_spec()

    # Set up API
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    app = connexion.AioHttpApp(__name__,
                               host=api_conf['host'],
                               port=api_conf['port'],
                               specification_dir=os.path.join(
                                   api_path[0], 'spec'),
                               options={
                                   "swagger_ui": False,
                                   'uri_parser_class': APIUriParser
                               },
                               only_one_api=True)
    app.add_api('spec.yaml',
                arguments={
                    'title': 'Wazuh API',
                    'protocol':
                    'https' if api_conf['https']['enabled'] else 'http',
                    'host': api_conf['host'],
                    'port': api_conf['port']
                },
                strict_validation=True,
                validate_responses=False,
                pass_context_arg_name='request',
                options={
                    "middlewares": [
                        response_postprocessing, set_user_name,
                        security_middleware, request_logging,
                        set_secure_headers
                    ]
                })

    # Enable CORS
    if api_conf['cors']['enabled']:
        import aiohttp_cors
        cors = aiohttp_cors.setup(
            app.app,
            defaults={
                api_conf['cors']['source_route']:
                aiohttp_cors.ResourceOptions(
                    expose_headers=api_conf['cors']['expose_headers'],
                    allow_headers=api_conf['cors']['allow_headers'],
                    allow_credentials=api_conf['cors']['allow_credentials'])
            })
        # Configure CORS on all endpoints.
        for route in list(app.app.router.routes()):
            cors.add(route)

    # Enable cache plugin
    if api_conf['cache']['enabled']:
        setup_cache(app.app)

    # API configuration logging
    logger.debug(f'Loaded API configuration: {api_conf}')
    logger.debug(f'Loaded security API configuration: {security_conf}')

    # Start API
    app.run(port=api_conf['port'],
            host=api_conf['host'],
            ssl_context=ssl_context,
            access_log_class=alogging.AccessLogger,
            use_default_access_log=True)
Exemple #8
0
def get_security_conf():
    conf.security_conf.update(
        conf.read_yaml_config(
            config_file=SECURITY_CONFIG_PATH,
            default_conf=conf.default_security_configuration))
    return copy.deepcopy(conf.security_conf)
Exemple #9
0
def start(foreground, root, config_file):
    """
    Run the Wazuh API.

    If another Wazuh API is running, this function fails. The `stop` command should be used first.
    This function exits with 0 if success or 2 if failed because the API was already running.

    Arguments
    ---------
    foreground : bool
        If the API must be daemonized or not
    root : bool
        If true, the daemon is run as root. Normally not recommended for security reasons
    config_file : str
        Path to the API config file
    """

    pids = get_wazuh_apid_pids()
    if pids:
        print(
            f"Cannot start API while other processes are running. Kill these before {pids}"
        )
        sys.exit(2)

    configuration.api_conf.update(
        configuration.read_yaml_config(config_file=args.config_file))
    api_conf = configuration.api_conf
    cors = api_conf['cors']
    log_path = api_conf['logs']['path']

    ssl_context = None
    if api_conf['https']['enabled'] and os.path.exists(api_conf['https']['key']) and \
            os.path.exists(api_conf['https']['cert']):
        try:
            ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS)
            if api_conf['https']['use_ca']:
                ssl_context.verify_mode = ssl.CERT_REQUIRED
                ssl_context.load_verify_locations(api_conf['https']['ca'])
            ssl_context.load_cert_chain(certfile=api_conf['https']['cert'],
                                        keyfile=api_conf['https']['key'])
        except ssl.SSLError as e:
            raise APIException(
                2003,
                details='Private key does not match with the certificate')
        except OSError as e:
            if e.errno == 22:
                raise APIException(2003, details='PEM phrase is not correct')

    # Foreground/Daemon
    if not foreground:
        print(f"Starting API in background")
        pyDaemonModule.pyDaemon()

    # Drop privileges to ossec
    if not root:
        if api_conf['drop_privileges']:
            os.setgid(common.ossec_gid())
            os.setuid(common.ossec_uid())

    set_logging(log_path=log_path,
                debug_mode=api_conf['logs']['level'],
                foreground_mode=args.foreground)

    # set correct permissions on api.log file
    if os.path.exists(os.path.join(common.ossec_path, log_path)):
        os.chown(os.path.join(common.ossec_path, log_path), common.ossec_uid(),
                 common.ossec_gid())
        os.chmod(os.path.join(common.ossec_path, log_path), 0o660)

    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    app = connexion.AioHttpApp(__name__,
                               host=api_conf['host'],
                               port=api_conf['port'],
                               specification_dir=os.path.join(
                                   api_path[0], 'spec'),
                               options={"swagger_ui": False})
    app.add_api('spec.yaml',
                arguments={
                    'title': 'Wazuh API',
                    'protocol':
                    'https' if api_conf['https']['enabled'] else 'http',
                    'host': api_conf['host'],
                    'port': api_conf['port']
                },
                strict_validation=True,
                validate_responses=True,
                pass_context_arg_name='request',
                options={"middlewares": [set_user_name, check_experimental]})

    # Enable CORS
    if cors['enabled']:
        cors = aiohttp_cors.setup(
            app.app,
            defaults={
                cors['source_route']:
                aiohttp_cors.ResourceOptions(
                    expose_headers=cors['expose_headers'],
                    allow_headers=cors['allow_headers'],
                    allow_credentials=cors['allow_credentials'])
            })
        # Configure CORS on all endpoints.
        for route in list(app.app.router.routes()):
            cors.add(route)

    # Enable cache plugin
    setup_cache(app.app)

    # Enable swagger UI plugin
    setup_swagger(app.app,
                  ui_version=3,
                  swagger_url='/ui',
                  swagger_from_file=os.path.join(app.specification_dir,
                                                 'spec.yaml'))

    # Configure https
    if api_conf['https']['enabled']:

        # Generate SSC if it does not exist and HTTPS is enabled
        if not os.path.exists(api_conf['https']['key']) or \
                not os.path.exists(api_conf['https']['cert']):
            logger = logging.getLogger('wazuh')
            logger.info(
                'HTTPS is enabled but cannot find the private key and/or certificate. '
                'Attempting to generate them.')
            private_key = generate_private_key(api_conf['https']['key'])
            logger.info(
                f"Generated private key file in WAZUH_PATH/{to_relative_path(api_conf['https']['key'])}."
            )
            generate_self_signed_certificate(private_key,
                                             api_conf['https']['cert'])
            logger.info(
                f"Generated certificate file in WAZUH_PATH/{to_relative_path(api_conf['https']['cert'])}."
            )

        if ssl_context is None:
            try:
                ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS)
                if api_conf['https']['use_ca']:
                    ssl_context.verify_mode = ssl.CERT_REQUIRED
                    ssl_context.load_verify_locations(api_conf['https']['ca'])
                ssl_context.load_cert_chain(certfile=api_conf['https']['cert'],
                                            keyfile=api_conf['https']['key'])
            except ssl.SSLError as e:
                raise APIException(
                    2003,
                    details='Private key does not match with the certificate')
            except IOError as e:
                raise APIException(
                    2003,
                    details=
                    'Please, ensure if path to certificates is correct in the configuration '
                    f'file WAZUH_PATH/{to_relative_path(CONFIG_FILE_PATH)}')

    app.run(port=api_conf['port'],
            host=api_conf['host'],
            ssl_context=ssl_context,
            access_log_class=alogging.AccessLogger,
            use_default_access_log=True)
Exemple #10
0
def start(foreground: bool, root: bool, config_file: str):
    """Run the Wazuh API.

    If another Wazuh API is running, this function fails.
    This function exits with 0 if successful or 1 if failed because the API was already running.

    Arguments
    ---------
    foreground : bool
        If the API must be daemonized or not
    root : bool
        If true, the daemon is run as root. Normally not recommended for security reasons
    config_file : str
        Path to the API config file
    """
    import logging
    import os
    from api import alogging, configuration
    from wazuh.core import pyDaemonModule, common, utils

    def set_logging(log_path='logs/api.log',
                    foreground_mode=False,
                    debug_mode='info'):
        for logger_name in ('connexion.aiohttp_app',
                            'connexion.apis.aiohttp_api', 'wazuh-api'):
            api_logger = alogging.APILogger(
                log_path=log_path,
                foreground_mode=foreground_mode,
                logger_name=logger_name,
                debug_level='info' if logger_name != 'wazuh-api'
                and debug_mode != 'debug2' else debug_mode)
            api_logger.setup_logger()

    if config_file is not None:
        configuration.api_conf.update(
            configuration.read_yaml_config(config_file=config_file))
    api_conf = configuration.api_conf
    security_conf = configuration.security_conf

    # Set up logger
    set_logging(log_path=API_LOG_FILE_PATH,
                debug_mode=api_conf['logs']['level'],
                foreground_mode=foreground)
    logger = logging.getLogger('wazuh-api')

    import asyncio
    import ssl

    import connexion
    import uvloop
    from aiohttp_cache import setup_cache
    from api import __path__ as api_path
    # noinspection PyUnresolvedReferences
    from api import validator
    from api.api_exception import APIError
    from api.constants import CONFIG_FILE_PATH
    from api.middlewares import set_user_name, security_middleware, response_postprocessing, request_logging, \
        set_secure_headers
    from api.signals import modify_response_headers
    from api.uri_parser import APIUriParser
    from api.util import to_relative_path
    from wazuh.rbac.orm import create_rbac_db

    # Check deprecated options. To delete after expected versions
    if 'use_only_authd' in api_conf:
        del api_conf['use_only_authd']
        logger.warning(
            "'use_only_authd' option was deprecated on v4.3.0. Wazuh Authd will always be used"
        )

    if 'path' in api_conf['logs']:
        del api_conf['logs']['path']
        logger.warning(
            "Log 'path' option was deprecated on v4.3.0. Default path will always be used: "
            f"{API_LOG_FILE_PATH}")

    # Set correct permissions on api.log file
    if os.path.exists(os.path.join(common.wazuh_path, API_LOG_FILE_PATH)):
        os.chown(os.path.join(common.wazuh_path, API_LOG_FILE_PATH),
                 common.wazuh_uid(), common.wazuh_gid())
        os.chmod(os.path.join(common.wazuh_path, API_LOG_FILE_PATH), 0o660)

    # Configure https
    ssl_context = None
    if api_conf['https']['enabled']:
        try:
            # Generate SSL if it does not exist and HTTPS is enabled
            if not os.path.exists(
                    api_conf['https']['key']) or not os.path.exists(
                        api_conf['https']['cert']):
                logger.info(
                    'HTTPS is enabled but cannot find the private key and/or certificate. '
                    'Attempting to generate them')
                private_key = configuration.generate_private_key(
                    api_conf['https']['key'])
                logger.info(
                    f"Generated private key file in WAZUH_PATH/{to_relative_path(api_conf['https']['key'])}"
                )
                configuration.generate_self_signed_certificate(
                    private_key, api_conf['https']['cert'])
                logger.info(
                    f"Generated certificate file in WAZUH_PATH/{to_relative_path(api_conf['https']['cert'])}"
                )

            # Load SSL context
            allowed_ssl_protocols = {
                'tls': ssl.PROTOCOL_TLS,
                'tlsv1': ssl.PROTOCOL_TLSv1,
                'tlsv1.1': ssl.PROTOCOL_TLSv1_1,
                'tlsv1.2': ssl.PROTOCOL_TLSv1_2
            }

            ssl_protocol = allowed_ssl_protocols[api_conf['https']
                                                 ['ssl_protocol'].lower()]
            ssl_context = ssl.SSLContext(protocol=ssl_protocol)

            if api_conf['https']['use_ca']:
                ssl_context.verify_mode = ssl.CERT_REQUIRED
                ssl_context.load_verify_locations(api_conf['https']['ca'])

            ssl_context.load_cert_chain(certfile=api_conf['https']['cert'],
                                        keyfile=api_conf['https']['key'])

            # Load SSL ciphers if any has been specified
            if api_conf['https']['ssl_ciphers']:
                ssl_ciphers = api_conf['https']['ssl_ciphers'].upper()
                try:
                    ssl_context.set_ciphers(ssl_ciphers)
                except ssl.SSLError:
                    error = APIError(2003,
                                     details='SSL ciphers cannot be selected')
                    logger.error(error)
                    raise error

        except ssl.SSLError:
            error = APIError(
                2003,
                details='Private key does not match with the certificate')
            logger.error(error)
            raise error
        except IOError as exc:
            if exc.errno == 22:
                error = APIError(2003, details='PEM phrase is not correct')
                logger.error(error)
                raise error
            elif exc.errno == 13:
                error = APIError(
                    2003,
                    details=
                    'Ensure the certificates have the correct permissions')
                logger.error(error)
                raise error
            else:
                msg = f'Wazuh API SSL ERROR. Please, ensure if path to certificates is correct in the configuration ' \
                      f'file WAZUH_PATH/{to_relative_path(CONFIG_FILE_PATH)}'
                print(msg)
                logger.error(msg)
                raise exc

    utils.check_pid('wazuh-apid')
    # Drop privileges to ossec
    if not root:
        if api_conf['drop_privileges']:
            os.setgid(common.wazuh_gid())
            os.setuid(common.wazuh_uid())
    else:
        print(f"Starting API as root")

    # Foreground/Daemon
    if not foreground:
        pyDaemonModule.pyDaemon()
        pid = os.getpid()
        pyDaemonModule.create_pid('wazuh-apid', pid) or register(
            pyDaemonModule.delete_pid, 'wazuh-apid', pid)
    else:
        print(f"Starting API in foreground")
    create_rbac_db()

    # Spawn child processes with their own needed imports
    if 'thread_pool' not in common.mp_pools.get():
        loop = asyncio.get_event_loop()
        loop.run_until_complete(
            asyncio.wait([
                loop.run_in_executor(
                    pool, getattr(sys.modules[__name__], f'spawn_{name}'))
                for name, pool in common.mp_pools.get().items()
            ]))

    # Set up API
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    app = connexion.AioHttpApp(__name__,
                               host=api_conf['host'],
                               port=api_conf['port'],
                               specification_dir=os.path.join(
                                   api_path[0], 'spec'),
                               options={
                                   "swagger_ui": False,
                                   'uri_parser_class': APIUriParser
                               },
                               only_one_api=True)
    app.add_api('spec.yaml',
                arguments={
                    'title': 'Wazuh API',
                    'protocol':
                    'https' if api_conf['https']['enabled'] else 'http',
                    'host': api_conf['host'],
                    'port': api_conf['port']
                },
                strict_validation=True,
                validate_responses=False,
                pass_context_arg_name='request',
                options={
                    "middlewares": [
                        response_postprocessing, set_user_name,
                        security_middleware, request_logging,
                        set_secure_headers
                    ]
                })

    # Maximum body size that the API can accept (bytes)
    app.app._client_max_size = configuration.api_conf['max_upload_size']

    # Enable CORS
    if api_conf['cors']['enabled']:
        import aiohttp_cors
        cors = aiohttp_cors.setup(
            app.app,
            defaults={
                api_conf['cors']['source_route']:
                aiohttp_cors.ResourceOptions(
                    expose_headers=api_conf['cors']['expose_headers'],
                    allow_headers=api_conf['cors']['allow_headers'],
                    allow_credentials=api_conf['cors']['allow_credentials'])
            })
        # Configure CORS on all endpoints.
        for route in list(app.app.router.routes()):
            cors.add(route)

    # Enable cache plugin
    if api_conf['cache']['enabled']:
        setup_cache(app.app)

    # Add application signals
    app.app.on_response_prepare.append(modify_response_headers)

    # API configuration logging
    logger.debug(f'Loaded API configuration: {api_conf}')
    logger.debug(f'Loaded security API configuration: {security_conf}')

    # Start API
    try:
        app.run(port=api_conf['port'],
                host=api_conf['host'],
                ssl_context=ssl_context,
                access_log_class=alogging.AccessLogger,
                use_default_access_log=True)
    except OSError as exc:
        if exc.errno == 98:
            error = APIError(2010)
            logger.error(error)
            raise error
        else:
            logger.error(exc)
            raise exc
Exemple #11
0
#!/usr/bin/env python

# Copyright (C) 2015-2020, Wazuh Inc.
# Created by Wazuh, Inc. <*****@*****.**>.
# This program is free software; you can redistribute it and/or modify it under the terms of GPLv2
from api import configuration
configuration.api_conf.update(configuration.read_yaml_config())

import argparse
import asyncio
import logging
import os
import sys

import wazuh.core.cluster.cluster
import wazuh.core.cluster.utils as cluster_utils
from wazuh.core import pyDaemonModule, common, configuration
from wazuh.core.cluster import __version__, __author__, __ossec_name__, __licence__, master, local_server, worker

#
# Aux functions
#


def set_logging(foreground_mode=False, debug_mode=0):
    cluster_logger = cluster_utils.ClusterLogger(
        foreground_mode=foreground_mode,
        log_path='logs/cluster.log',
        debug_level=debug_mode,
        tag='{asctime} {levelname}: [{tag}] [{subtag}] {message}')
    cluster_logger.setup_logger()