Ejemplo n.º 1
0
def read_yaml_config(config_file=common.api_config_path,
                     default_conf=None) -> Dict:
    """Reads user API configuration and merges it with the default one

    :return: API configuration
    """
    if default_conf is None:
        default_conf = default_api_configuration

    if os.path.exists(config_file):
        try:
            with open(config_file) as f:
                configuration = yaml.safe_load(f)
        except IOError as e:
            raise APIError(2004, details=e.strerror)
    else:
        configuration = None

    # If any value is missing from user's cluster configuration, add the default one:
    if configuration is None:
        configuration = copy.deepcopy(default_conf)
    else:
        dict_to_lowercase(configuration)
        configuration = fill_dict(default_conf, configuration)

    # Append ossec_path to all paths in configuration
    append_ossec_path(configuration, [('logs', 'path'), ('https', 'key'),
                                      ('https', 'cert'), ('https', 'ca')])

    return configuration
Ejemplo n.º 2
0
def fill_dict(default: Dict, config: Dict, json_schema: Dict) -> Dict:
    """Validate and fill a dictionary's missing values using default ones.

    Parameters
    ----------
    default : dict
        Dictionary with default values.
    config : dict
        Dictionary to be filled.
    json_schema : dict
        Jsonschema with allowed properties.

    Returns
    -------
    dict
        Filled dictionary.
    """
    try:
        validate(instance=config, schema=json_schema)
    except ValidationError as e:
        raise APIError(2000, details=e.message)

    for k, val in filter(lambda x: isinstance(x[1], dict), config.items()):
        for item, value in config[k].items():
            config[k][item] = default[k][item] if value == "" else config[k][item]
        config[k] = {**default[k], **config[k]}

    return {**default, **config}
Ejemplo n.º 3
0
async def check_experimental(request, handler):
    if 'experimental' in request.path:
        if not configuration.api_conf['experimental_features']:
            raise_if_exc(APIError(code=2008))

    response = await handler(request)
    return response
Ejemplo n.º 4
0
        def wrapper(request):
            def coerce_dict(md):
                """ MultiDict -> dict of lists
                """
                try:
                    return md.to_dict(flat=False)
                except AttributeError:
                    return dict(md.items())

            # Raise exception if semicolon is used in q parameter
            if 'q' in request.query.keys():
                q = parse_api_param(request.url, 'q')
                if q:
                    if ';' in q:
                        raise_if_exc(APIError(code=2009))

            # Transform to lowercase the values for query parameter's spec.yaml enums
            lower_fields = ['component', 'configuration', 'hash', 'requirement', 'status', 'type', 'section', 'tag',
                            'level', 'resource']
            request.query.update(
                {k.lower(): [list_item.lower() for list_item in v] if isinstance(v, list) else v.lower()
                 for k, v in request.query.items() if k in lower_fields})

            query = coerce_dict(request.query)
            path_params = coerce_dict(request.path_params)
            form = coerce_dict(request.form)

            request.query = self.resolve_query(query)
            request.path_params = self.resolve_path(path_params)
            request.form = self.resolve_form(form)
            response = function(request)
            return response
Ejemplo n.º 5
0
        def wrapper(request):
            def coerce_dict(md):
                """ MultiDict -> dict of lists
                """
                try:
                    return md.to_dict(flat=False)
                except AttributeError:
                    return dict(md.items())

            # Raise exception if semicolon is used in q parameter
            if 'q' in request.query.keys():
                q = parse_api_param(request.url, 'q')
                if q:
                    if ';' in q:
                        raise_if_exc(APIError(code=2009))

            query = coerce_dict(request.query)
            path_params = coerce_dict(request.path_params)
            form = coerce_dict(request.form)

            request.query = self.resolve_query(query)
            request.path_params = self.resolve_path(path_params)
            request.form = self.resolve_form(form)
            response = function(request)
            return response
Ejemplo n.º 6
0
def read_yaml_config(config_file=CONFIG_FILE_PATH, default_conf=None) -> Dict:
    """Reads user API configuration and merges it with the default one

    :return: API configuration
    """
    def replace_bools(conf):
        """Replace 'yes' and 'no' strings in configuration for actual booleans.

        Parameters
        ----------
        conf : dict
            Current API configuration.
        """
        for k in conf.keys():
            if isinstance(conf[k], dict):
                replace_bools(conf[k])
            else:
                if isinstance(conf[k], str):
                    if conf[k].lower() == 'yes':
                        conf[k] = True
                    elif conf[k].lower() == 'no':
                        conf[k] = False

    if default_conf is None:
        default_conf = default_api_configuration

    if config_file and os.path.exists(config_file):
        try:
            with open(config_file) as f:
                configuration = yaml.safe_load(f)
            # Replace strings for booleans
            configuration and replace_bools(configuration)
        except IOError as e:
            raise APIError(2004, details=e.strerror)
    else:
        configuration = None

    if configuration is None:
        configuration = copy.deepcopy(default_conf)
    else:
        # If any value is missing from user's configuration, add the default one:
        dict_to_lowercase(configuration)
        schema = security_config_schema if config_file == SECURITY_CONFIG_PATH else api_config_schema
        configuration = fill_dict(default_conf, configuration, schema)

    # Append Wazuh prefixes to all relative paths in configuration
    append_wazuh_prefixes(
        configuration,
        {API_SSL_PATH: [('https', 'key'), ('https', 'cert'), ('https', 'ca')]})

    return configuration
Ejemplo n.º 7
0
def read_yaml_config(config_file=common.api_config_path,
                     default_conf=None) -> Dict:
    """Reads user API configuration and merges it with the default one

    :return: API configuration
    """
    def replace_bools(conf):
        """Replace 'yes' and 'no' strings in configuration for actual booleans.

        Parameters
        ----------
        conf : dict
            Current API configuration.
        """
        for k in conf.keys():
            if isinstance(conf[k], dict):
                replace_bools(conf[k])
            else:
                if isinstance(conf[k], str):
                    if conf[k].lower() == 'yes':
                        conf[k] = True
                    elif conf[k].lower() == 'no':
                        conf[k] = False

    if default_conf is None:
        default_conf = default_api_configuration

    if os.path.exists(config_file):
        try:
            with open(config_file) as f:
                configuration = yaml.safe_load(f)
            # Replace strings for booleans
            configuration and replace_bools(configuration)
        except IOError as e:
            raise APIError(2004, details=e.strerror)
    else:
        configuration = None

    # If any value is missing from user's cluster configuration, add the default one:
    if configuration is None:
        configuration = copy.deepcopy(default_conf)
    else:
        dict_to_lowercase(configuration)
        configuration = fill_dict(default_conf, configuration)

    # Append ossec_path to all paths in configuration
    append_ossec_path(configuration, [('logs', 'path'), ('https', 'key'),
                                      ('https', 'cert'), ('https', 'ca')])

    return configuration
Ejemplo n.º 8
0
async def update_policy(request,
                        policy_id: int,
                        pretty: bool = False,
                        wait_for_complete: bool = False):
    """Update the information of one specified policy.

    Parameters
    ----------
    request : connexion.request
    policy_id : int
        Specific policy id in the system to be updated
    pretty : bool, optional
        Show results in human-readable format
    wait_for_complete : bool, optional
        Disable timeout response

    Returns
    -------
    Policy information updated
    """
    # get body parameters
    policy_added_model = dict()
    try:
        policy_added_model = await request.json()
    except JSONDecodeError as e:
        raise_if_exc(APIError(code=2005, details=e.msg))

    f_kwargs = {
        'policy_id': policy_id,
        'name': policy_added_model.get('name', None),
        'policy': policy_added_model.get('policy', None)
    }

    dapi = DistributedAPI(
        f=security.update_policy,
        f_kwargs=remove_nones_to_dict(f_kwargs),
        request_type='local_master',
        is_async=False,
        wait_for_complete=wait_for_complete,
        logger=logger,
        rbac_permissions=request['token_info']['rbac_policies'])
    data = raise_if_exc(await dapi.distribute_function())

    return web.json_response(data=data,
                             status=200,
                             dumps=prettify if pretty else dumps)
Ejemplo n.º 9
0
def fill_dict(default: Dict, config: Dict) -> Dict:
    """Fills a dictionary's missing values using default ones.

    :param default: Dictionary with default values
    :param config: Dictionary to fill
    :return: Filled dictionary
    """
    # Check there aren't extra configuration values in user's configuration:
    for k in config.keys():
        if k not in default.keys():
            raise APIError(2000, details=', '.join(config.keys() - default.keys()))

    for k, val in filter(lambda x: isinstance(x[1], dict), config.items()):
        for item, value in config[k].items():
            config[k][item] = default[k][item] if value == "" else config[k][item]
        config[k] = {**default[k], **config[k]}

    return {**default, **config}
Ejemplo n.º 10
0
async def add_role(request,
                   pretty: bool = False,
                   wait_for_complete: bool = False):
    """Add one specified role.

    Parameters
    ----------
    request : request.connexion
    pretty : bool, optional
        Show results in human-readable format
    wait_for_complete : bool, optional
        Disable timeout response

    Returns
    -------
    Role information
    """
    # get body parameters
    role_added_model = dict()
    try:
        role_added_model = await request.json()
    except JSONDecodeError as e:
        raise_if_exc(APIError(code=2005, details=e.msg))

    f_kwargs = {
        'name': role_added_model['name'],
        'rule': role_added_model['rule']
    }

    dapi = DistributedAPI(
        f=security.add_role,
        f_kwargs=remove_nones_to_dict(f_kwargs),
        request_type='local_master',
        is_async=False,
        wait_for_complete=wait_for_complete,
        logger=logger,
        rbac_permissions=request['token_info']['rbac_policies'])
    data = raise_if_exc(await dapi.distribute_function())

    return web.json_response(data=data,
                             status=200,
                             dumps=prettify if pretty else dumps)
Ejemplo n.º 11
0
    async def get_kwargs(cls, request, additional_kwargs: dict = None):
        try:
            dikt = request if isinstance(request,
                                         dict) else await request.json()
            f_kwargs = util.deserialize_model(dikt, cls).to_dict()
        except JSONDecodeError as e:
            raise_if_exc(APIError(code=2005, details=e.msg))

        invalid = {
            key
            for key in dikt.keys() if key not in list(f_kwargs.keys())
        }

        if invalid:
            raise_if_exc(
                WazuhError(
                    5005,
                    extra_message='Invalid field found {}'.format(invalid)))

        if additional_kwargs is not None:
            f_kwargs.update(additional_kwargs)

        return f_kwargs
Ejemplo n.º 12
0
async def put_api_config(request, pretty=False, wait_for_complete=False):
    """Update current API configuration with the given one.

    :param pretty: Show results in human-readable format
    :param wait_for_complete: Disable timeout response
    """
    try:
        f_kwargs = {"updated_config": await request.json()}
    except JSONDecodeError as e:
        raise_if_exc(APIError(code=2005, details=e.msg))

    dapi = DistributedAPI(
        f=manager.update_api_config,
        f_kwargs=remove_nones_to_dict(f_kwargs),
        request_type='local_any',
        is_async=False,
        wait_for_complete=wait_for_complete,
        logger=logger,
        rbac_permissions=request['token_info']['rbac_policies'])
    data = raise_if_exc(await dapi.distribute_function())

    return web.json_response(data=data,
                             status=200,
                             dumps=prettify if pretty else dumps)
Ejemplo n.º 13
0
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)
Ejemplo n.º 14
0
        except IOError as e:
            raise APIError(2004, details=e.strerror)
    else:
        configuration = None

    if configuration is None:
        configuration = copy.deepcopy(default_conf)
    else:
        # If any value is missing from user's configuration, add the default one:
        dict_to_lowercase(configuration)
        schema = security_config_schema if config_file == SECURITY_CONFIG_PATH else api_config_schema
        configuration = fill_dict(default_conf, configuration, schema)

    # Append wazuh_path to all paths in configuration
    append_wazuh_path(configuration, [('logs', 'path'), ('https', 'key'), ('https', 'cert'), ('https', 'ca')])

    return configuration


# Check if the default configuration is valid according to its jsonschema, so we are forced to update the schema if any
# change is performed to the configuration.
try:
    validate(instance=default_security_configuration, schema=security_config_schema)
    validate(instance=default_api_configuration, schema=api_config_schema)
except ValidationError as e:
    raise APIError(2000, details=e.message)

# Configuration - global object
api_conf = dict()
security_conf = read_yaml_config(config_file=SECURITY_CONFIG_PATH, default_conf=default_security_configuration)
Ejemplo n.º 15
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
Ejemplo n.º 16
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
    """
    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 APIError(2003, details='Private key does not match with the certificate')
        except OSError as e:
            if e.errno == 22:
                raise APIError(2003, details='PEM phrase is not correct')

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

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

    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)

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

    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=True,
                pass_context_arg_name='request',
                options={"middlewares": [response_postprocessing, set_user_name, security_middleware]})

    # 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:
                raise APIError(2003, details='Private key does not match with the certificate')
            except IOError:
                raise APIError(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
            )