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.wazuh_uid(), common.wazuh_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 (<wazuh_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.wazuh_uid(), common.wazuh_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 <wazuh_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.wazuh_uid(), common.wazuh_gid()))
def generate_keypair(): """Generate key files to keep safe or load existing public and private keys.""" try: if not os.path.exists(_private_key_path) or not os.path.exists( _public_key_path): private_key, public_key = change_keypair() try: os.chown(_private_key_path, wazuh_uid(), wazuh_gid()) os.chown(_public_key_path, wazuh_uid(), wazuh_gid()) except PermissionError: pass os.chmod(_private_key_path, 0o640) os.chmod(_public_key_path, 0o640) else: with open(_private_key_path, mode='r') as key_file: private_key = key_file.read() with open(_public_key_path, mode='r') as key_file: public_key = key_file.read() except IOError: raise WazuhInternalError(6003) return private_key, public_key
def set_agent_group_file(agent_id, group_id): try: agent_group_path = path.join(common.groups_path, agent_id) new_file = not path.exists(agent_group_path) with open(agent_group_path, 'w') as f_group: f_group.write(group_id) if new_file: chown(agent_group_path, common.wazuh_uid(), common.wazuh_gid()) chmod(agent_group_path, 0o660) except Exception as e: raise WazuhInternalError(1005, extra_message=str(e))
def generate_secret(): """Generate secret file to keep safe or load existing secret.""" try: if not os.path.exists(_secret_file_path): jwt_secret = token_urlsafe(512) with open(_secret_file_path, mode='x') as secret_file: secret_file.write(jwt_secret) try: chown(_secret_file_path, wazuh_uid(), wazuh_gid()) except PermissionError: pass os.chmod(_secret_file_path, 0o640) else: with open(_secret_file_path, mode='r') as secret_file: jwt_secret = secret_file.readline() except IOError: raise WazuhInternalError(6003) return jwt_secret
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)
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.wazuh_uid(), common.wazuh_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)
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.wazuh_uid(), common.wazuh_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 # Let other tasks (DAPI, etc) that may arrive while processing extra-valid files to be run. await asyncio.sleep(0) # 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.wazuh_uid(), common.wazuh_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
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
def test_wazuh_gid(): with patch('wazuh.core.common.getgrnam', return_value=getgrnam("root")): wazuh_gid()
def process_files_from_worker(files_metadata: Dict, decompressed_files_path: str, cluster_items: dict, worker_name: str, timeout: int): """Iterate over received files from worker and updates the local ones. Parameters ---------- files_metadata : dict Dictionary containing file metadata (each key is a filepath and each value its metadata). decompressed_files_path : str Filepath of the decompressed received zipfile. cluster_items : dict Object containing cluster internal variables from the cluster.json file. worker_name : str Name of the worker instance. Used to access the correct worker folder. timeout : int Seconds to wait before stopping the task. Returns ------- result : dict Dict containing number of updated chunks and any error found in the process. """ result = { 'total_updated': 0, 'errors_per_folder': defaultdict(list), 'generic_errors': [] } try: with utils.Timeout(timeout): for file_path, data in files_metadata.items(): full_path = os.path.join(common.wazuh_path, file_path) item_key = data['cluster_item_key'] # Only valid client.keys is the local one (master). if os.path.basename(file_path) == 'client.keys': raise exception.WazuhClusterError(3007) # If the file is merged, create individual files from it. if data['merged']: for unmerged_file_path, file_data, file_time in wazuh.core.cluster.cluster.unmerge_info( data['merge_type'], decompressed_files_path, data['merge_name']): try: # Destination path. full_unmerged_name = os.path.join( common.wazuh_path, unmerged_file_path) # Path where to create the file before moving it to the destination path. tmp_unmerged_path = os.path.join( common.wazuh_path, 'queue', 'cluster', worker_name, os.path.basename(unmerged_file_path)) # 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 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: 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.wazuh_uid(), common.wazuh_gid()), permissions=cluster_items['files'] [item_key]['permissions'], time=(mtime_epoch, mtime_epoch)) result['total_updated'] += 1 except TimeoutError as e: raise e except Exception as e: result['errors_per_folder'][item_key].append( str(e)) # If the file is not 'merged' type, move it directly to the destination path. else: try: zip_path = os.path.join(decompressed_files_path, file_path) utils.safe_move(zip_path, full_path, ownership=(common.wazuh_uid(), common.wazuh_gid()), permissions=cluster_items['files'] [item_key]['permissions']) except TimeoutError as e: raise e except Exception as e: result['errors_per_folder'][item_key].append( str(e)) except TimeoutError: result['generic_errors'].append( "Timeout processing extra-valid files.") except Exception as e: result['generic_errors'].append( f"Error updating worker files (extra valid): '{str(e)}'.") return result