def overwrite_or_create_files(filename: str, data: Dict): """ Updates a file coming from the master :param filename: Filename to update :param data: File metadata such as modification time, whether it's a merged file or not, etc. :return: None """ full_filename_path = common.ossec_path + filename if os.path.basename(filename) == 'client.keys': self._check_removed_agents("{}{}".format(zip_path, filename), logger) if data['merged']: # worker nodes can only receive agent-groups files if data['merge-type'] == 'agent-info': logger.warning("Agent status received in a worker node") raise WazuhInternalError(3011) for name, content, _ in wazuh.core.cluster.cluster.unmerge_agent_info('agent-groups', zip_path, filename): full_unmerged_name = os.path.join(common.ossec_path, name) tmp_unmerged_path = full_unmerged_name + '.tmp' with open(tmp_unmerged_path, 'wb') as f: f.write(content) safe_move(tmp_unmerged_path, full_unmerged_name, permissions=self.cluster_items['files'][data['cluster_item_key']]['permissions'], ownership=(common.ossec_uid(), common.ossec_gid()) ) else: if not os.path.exists(os.path.dirname(full_filename_path)): utils.mkdir_with_mode(os.path.dirname(full_filename_path)) safe_move("{}{}".format(zip_path, filename), full_filename_path, permissions=self.cluster_items['files'][data['cluster_item_key']]['permissions'], ownership=(common.ossec_uid(), common.ossec_gid()) )
def create_group(group_id): """Creates a group. :param group_id: Group ID. :return: Confirmation message. """ # Input Validation of group_id if not InputValidator().group(group_id): raise WazuhError(1722) group_path = path.join(common.shared_path, group_id) if group_id.lower() == "default" or path.exists(group_path): raise WazuhError(1711, extra_message=group_id) # Create group in /etc/shared group_def_path = path.join(common.shared_path, 'agent-template.conf') try: mkdir_with_mode(group_path) copyfile(group_def_path, path.join(group_path, 'agent.conf')) chown_r(group_path, common.ossec_uid(), common.ossec_gid()) chmod_r(group_path, 0o660) chmod(group_path, 0o770) msg = f"Group '{group_id}' created." except Exception as e: raise WazuhInternalError(1005, extra_message=str(e)) return WazuhResult({'message': msg})
def overwrite_or_create_files(filename: str, data: Dict): """Update a file coming from the master. Move a file which is inside the unzipped directory that comes from master to the path specified in 'filename'. If the file is 'merged' type, it is first split into files and then moved to their final directory. Parameters ---------- filename : str Filename inside unzipped dir to update. data : dict File metadata such as modification time, whether it's a merged file or not, etc. """ full_filename_path = os.path.join(common.wazuh_path, filename) if os.path.basename(filename) == 'client.keys': self._check_removed_agents(os.path.join(zip_path, filename), logger) if data['merged']: # worker nodes can only receive agent-groups files # Split merged file into individual files inside zipdir (directory containing unzipped files), # and then move each one to the destination directory (<ossec_path>/filename). for name, content, _ in wazuh.core.cluster.cluster.unmerge_info( 'agent-groups', zip_path, filename): full_unmerged_name = os.path.join(common.wazuh_path, name) tmp_unmerged_path = full_unmerged_name + '.tmp' with open(tmp_unmerged_path, 'wb') as f: f.write(content) safe_move(tmp_unmerged_path, full_unmerged_name, permissions=self.cluster_items['files'][ data['cluster_item_key']]['permissions'], ownership=(common.ossec_uid(), common.ossec_gid())) else: # Create destination dir if it doesn't exist. if not os.path.exists(os.path.dirname(full_filename_path)): utils.mkdir_with_mode(os.path.dirname(full_filename_path)) # Move the file from zipdir (directory containing unzipped files) to <ossec_path>/filename. safe_move(os.path.join(zip_path, filename), full_filename_path, permissions=self.cluster_items['files'][ data['cluster_item_key']]['permissions'], ownership=(common.ossec_uid(), common.ossec_gid()))
def write_into_yaml_file(config: Dict): """ Writes old configuration into a YAML file :param config: Dictionary with old configuration values """ json_config = json.dumps(config) try: with open(CONFIG_FILE_PATH, 'w') as output_file: yaml.dump(json.loads(json_config), output_file, default_flow_style=False, allow_unicode=True) # change group and permissions from config.yml file os.chown(CONFIG_FILE_PATH, common.ossec_uid(), common.ossec_gid()) os.chmod(CONFIG_FILE_PATH, 0o660) except IOError as e: raise APIException( 2002, details='API configuration could not be written into ' f'{to_relative_path(CONFIG_FILE_PATH)} file: ' f'{e.strerror}')
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.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) # 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 ossec if not root: if api_conf['drop_privileges']: os.setgid(common.ossec_gid()) os.setuid(common.ossec_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 )
async def update_file(name: str, data: Dict): """Update a local file with one received from a worker. The modification date is checked to decide whether to update ir or not. Parameters ---------- name : str Relative path of the file. data : dict Metadata of the file (MD5, merged, etc). """ # Full path full_path, error_updating_file = os.path.join( common.wazuh_path, name), False try: # Only valid client.keys is the local one (master). if os.path.basename(name) == 'client.keys': self.logger.warning( "Client.keys received in a master node") raise exception.WazuhClusterError(3007) # If the file is merged, create individual files from it. if data['merged']: for file_path, file_data, file_time in wazuh.core.cluster.cluster.unmerge_info( data['merge_type'], decompressed_files_path, data['merge_name']): # Destination path. full_unmerged_name = os.path.join( common.wazuh_path, file_path) # Path where to create the file before moving it to the destination path (with safe_move). tmp_unmerged_path = os.path.join( common.wazuh_path, 'queue', 'cluster', self.name, os.path.basename(file_path)) try: agent_id = os.path.basename(file_path) # If the agent does not exist on the master, do not copy its file from the worker. if agent_id not in agent_ids: n_errors['warnings'][data['cluster_item_key']] = 1 \ if n_errors['warnings'].get(data['cluster_item_key']) is None \ else n_errors['warnings'][data['cluster_item_key']] + 1 self.logger.debug2( f"Received group of an non-existent agent '{agent_id}'" ) continue # Format the file_data specified inside the merged file. try: mtime = datetime.strptime( file_time, '%Y-%m-%d %H:%M:%S.%f') except ValueError: mtime = datetime.strptime( file_time, '%Y-%m-%d %H:%M:%S') # If the file already existed, check if it is older than the one to be copied from worker. if os.path.isfile(full_unmerged_name): local_mtime = datetime.utcfromtimestamp( int(os.stat(full_unmerged_name).st_mtime)) if local_mtime > mtime: logger.debug2( f"Receiving an old file ({file_path})") continue # Create file in temporal path and safe move it to the destination path. with open(tmp_unmerged_path, 'wb') as f: f.write(file_data) mtime_epoch = timegm(mtime.timetuple()) utils.safe_move( tmp_unmerged_path, full_unmerged_name, ownership=(common.ossec_uid(), common.ossec_gid()), permissions=self.cluster_items['files'][ data['cluster_item_key']]['permissions'], time=(mtime_epoch, mtime_epoch)) self.integrity_sync_status[ 'total_extra_valid'] += 1 except Exception as e: self.logger.error( f"Error updating agent group/status ({tmp_unmerged_path}): {e}" ) n_errors['errors'][data['cluster_item_key']] = 1 \ if n_errors['errors'].get(data['cluster_item_key']) is None \ else n_errors['errors'][data['cluster_item_key']] + 1 await asyncio.sleep(0.0001) # If the file is not merged, move it directly to the destination path. else: zip_path = os.path.join(decompressed_files_path, name) utils.safe_move(zip_path, full_path, ownership=(common.ossec_uid(), common.ossec_gid()), permissions=self.cluster_items['files'] [data['cluster_item_key']]['permissions']) except exception.WazuhException as e: logger.debug2(f"Warning updating file '{name}': {e}") error_tag = 'warnings' error_updating_file = True except Exception as e: logger.debug2(f"Error updating file '{name}': {e}") error_tag = 'errors' error_updating_file = True if error_updating_file: n_errors[error_tag][data['cluster_item_key']] = 1 if not n_errors[error_tag].get( data['cluster_item_key']) \ else n_errors[error_tag][data['cluster_item_key']] + 1
async def update_file(name: str, data: Dict): """ Updates a file from the worker. It checks the modification date to decide whether to update it or not. If it's a merged file, it unmerges it. :param name: Filename to update :param data: File metadata :return: None """ # Full path full_path, error_updating_file, n_merged_files = common.ossec_path + name, False, 0 # Cluster items information: write mode and permissions lock_full_path = "{}/queue/cluster/lockdir/{}.lock".format(common.ossec_path, os.path.basename(full_path)) lock_file = open(lock_full_path, 'a+') try: fcntl.lockf(lock_file, fcntl.LOCK_EX) if os.path.basename(name) == 'client.keys': self.logger.warning("Client.keys received in a master node") raise exception.WazuhClusterError(3007) if data['merged']: self.sync_extra_valid_status['total_extra_valid'] = len(agent_ids) for file_path, file_data, file_time in wazuh.core.cluster.cluster.unmerge_info(data['merge_type'], decompressed_files_path, data['merge_name']): full_unmerged_name = os.path.join(common.ossec_path, file_path) tmp_unmerged_path = os.path.join(common.ossec_path, 'queue/cluster', self.name, os.path.basename(file_path)) try: agent_id = os.path.basename(file_path) if agent_id not in agent_ids: n_errors['warnings'][data['cluster_item_key']] = 1 \ if n_errors['warnings'].get(data['cluster_item_key']) is None \ else n_errors['warnings'][data['cluster_item_key']] + 1 self.logger.debug2("Received group of an non-existent agent '{}'".format(agent_id)) continue try: mtime = datetime.strptime(file_time, '%Y-%m-%d %H:%M:%S.%f') except ValueError: mtime = datetime.strptime(file_time, '%Y-%m-%d %H:%M:%S') if os.path.isfile(full_unmerged_name): local_mtime = datetime.utcfromtimestamp(int(os.stat(full_unmerged_name).st_mtime)) # check if the date is older than the manager's date if local_mtime > mtime: logger.debug2("Receiving an old file ({})".format(file_path)) continue with open(tmp_unmerged_path, 'wb') as f: f.write(file_data) mtime_epoch = timegm(mtime.timetuple()) utils.safe_move(tmp_unmerged_path, full_unmerged_name, ownership=(common.ossec_uid(), common.ossec_gid()), permissions=self.cluster_items['files'][data['cluster_item_key']]['permissions'], time=(mtime_epoch, mtime_epoch) ) except Exception as e: self.logger.error("Error updating agent group/status ({}): {}".format(tmp_unmerged_path, e)) self.sync_extra_valid_status['total_extra_valid'] -= 1 n_errors['errors'][data['cluster_item_key']] = 1 \ if n_errors['errors'].get(data['cluster_item_key']) is None \ else n_errors['errors'][data['cluster_item_key']] + 1 await asyncio.sleep(0.0001) else: zip_path = "{}{}".format(decompressed_files_path, name) utils.safe_move(zip_path, full_path, ownership=(common.ossec_uid(), common.ossec_gid()), permissions=self.cluster_items['files'][data['cluster_item_key']]['permissions'] ) except exception.WazuhException as e: logger.debug2("Warning updating file '{}': {}".format(name, e)) error_tag = 'warnings' error_updating_file = True except Exception as e: logger.debug2("Error updating file '{}': {}".format(name, e)) error_tag = 'errors' error_updating_file = True if error_updating_file: n_errors[error_tag][data['cluster_item_key']] = 1 if not n_errors[error_tag].get( data['cluster_item_key']) \ else n_errors[error_tag][data['cluster_item_key']] + 1 fcntl.lockf(lock_file, fcntl.LOCK_UN) lock_file.close()
def test_ossec_gid(): with patch('wazuh.core.common.getgrnam', return_value=getgrnam("root")): ossec_gid()
default=common.ossec_conf) args = parser.parse_args() if args.version: print_version() sys.exit(0) # Set logger try: debug_mode = configuration.get_internal_options_value('wazuh_clusterd', 'debug', 2, 0) or args.debug_level except Exception: debug_mode = 0 # set correct permissions on cluster.log file if os.path.exists('{0}/logs/cluster.log'.format(common.wazuh_path)): os.chown('{0}/logs/cluster.log'.format(common.wazuh_path), common.ossec_uid(), common.ossec_gid()) os.chmod('{0}/logs/cluster.log'.format(common.wazuh_path), 0o660) main_logger = set_logging(foreground_mode=args.foreground, debug_mode=debug_mode) cluster_configuration = cluster_utils.read_config(config_file=args.config_file) if cluster_configuration['disabled']: sys.exit(0) cluster_items = cluster_utils.get_cluster_items() try: wazuh.core.cluster.cluster.check_cluster_config(cluster_configuration) except Exception as e: main_logger.error(e) sys.exit(1) if args.test_config:
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)
if args.version: print_version() sys.exit(0) # Set logger try: debug_mode = configuration.get_internal_options_value( 'wazuh_clusterd', 'debug', 2, 0) or args.debug_level except Exception: debug_mode = 0 # set correct permissions on cluster.log file if os.path.exists('{0}/logs/cluster.log'.format(common.ossec_path)): os.chown('{0}/logs/cluster.log'.format(common.ossec_path), common.ossec_uid(), common.ossec_gid()) os.chmod('{0}/logs/cluster.log'.format(common.ossec_path), 0o660) main_logger = set_logging(foreground_mode=args.foreground, debug_mode=debug_mode) cluster_configuration = cluster_utils.read_config( config_file=args.config_file) if cluster_configuration['disabled']: sys.exit(0) cluster_items = cluster_utils.get_cluster_items() try: wazuh.core.cluster.cluster.check_cluster_config(cluster_configuration) except Exception as e: main_logger.error(e) sys.exit(1)