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)
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)
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)
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)
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)
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)
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
#!/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()